I'm testing deserializing to a collection object when my JsonNode no value. I want the object to be equal to null.
This is what I'm trying:
public class ImmutableDiscoveredUrlDeserializer extends JsonDeserializer<ImmutableDiscoveredUrl> {
String parentUrl;
Double parentUrlSentiment;
Set<String> childUrls;
Boolean isParentVendorUrl;
Map<TagClassification, Set<String>> parentUrlArticleTags;
/*
* (non-Javadoc)
* #see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
#Override
public ImmutableDiscoveredUrl deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
JsonNode node = p.readValueAsTree();
parentUrl = defaultIfNull(node.get("parentUrl").asText(), null);
childUrls = defaultIfNull(parseChildUrls(node), emptySet());
isParentVendorUrl = defaultIfNull(Boolean.valueOf(node.get("isParentVendorUrl").asText()), null);
parentUrlArticleTags = defaultIfNull(parseArticleTags(node.get("parentUrlArticleTags")), emptyMap());
return ImmutableDiscoveredUrl.discoveredUrl().parentUrl(parentUrl)
.parentUrlSentiment(parentUrlSentiment).childUrls(childUrls)
.isParentVendorUrl(isParentVendorUrl).parentUrlArticleTags(parentUrlArticleTags);
}
private Set<String> parseChildUrls(JsonNode node) throws IOException {
ObjectMapper tagsMapper = new ObjectMapper();
return tagsMapper.convertValue(node, new TypeReference<Set<String>>() {});
}
private Map<TagClassification, Set<String>> parseArticleTags(JsonNode node) throws IOException {
ObjectMapper tagsMapper = new ObjectMapper();
return tagsMapper.convertValue(node, new TypeReference<Set<String>>() {});
}
But I get a MismatchedInputException, stating that there's no content to map. How do I get the ObjectMapper to return a null?
Since you already have a JsonNode you can use ObjectMapper#convertValue:
#Test
public void converts_null_to_null() throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree("{\"foo\":null}");
JsonNode foo = jsonNode.get("foo");
Set<String> result = mapper.convertValue(foo, new TypeReference<Set<String>>() {});
assertNull(result);
}
Note that convertValue() will not work as intended if you pass in a plain Map. In your case you need to remove defaultIfNull and check for null yourself:
if (node.get("parentUrlArticleTags") !== null) {
parentUrlArticleTags = parseArticleTags(node.get("parentUrlArticleTags"));
}
In my case I had a little different issue, for getting
Cannot deserialize instance of `java.util.HashSet` out of START_OBJECT token
at [Source: (String)"{"bbc":"firstbckt","ssg":{"751fad0":"751fad0","be0eb99":"be0eb99"}}"
and here was my POJO
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.google.common.collect.Maps;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
#Data
#NoArgsConstructor
#AllArgsConstructor
#SuppressWarnings("serial")
public class BulkUploadRsp implements Serializable {
private String bbc;
private Map<String, String> ssgs = Maps.newHashMap();
//..
public void setSsgs(Set<String> ssgs) {
this.ssgs = ssgs.stream().collect(Collectors.toMap(ssg -> ssg, ssg -> ssg));
}
}
Issue was: I had this util method which starts with set*. Renaming it to setSsgs(Set<String> ssgs) helped resolve the issue
Related
I have a json like:
{
"names": "John, Tom",
"values": "8, 9",
"statuses": "yes, no"
}
and want to deserialize to:
class Bean {
private List<String> names;
private List<Integer> values;
private List<StatusEnum> statuses;
}
I know that implementing StringToStringListDeserializer, StringToIntegerListDeserializer, and StringToStatusEnumListDeserializer separately is practicable. But there are many other content types, including customized types. I tried:
public class StringToListDeserializer<T> extends JsonDeserializer<List<T>> implements ContextualDeserializer
public List<T> deserialize(JsonParser p, DeserializationContext context) throws IOException {
JavaType javaType = property.getType();
if (p.hasToken(JsonToken.VALUE_STRING)) {
String text = p.getText();
if (StringUtils.isBlank(text)) {
return null;
}
List<T> list = new LinkedList<>();
JavaType contentType = javaType.getContentType();
JsonDeserializer<Object> deserializer = context.findNonContextualValueDeserializer(contentType);
for (String s : text.split(DELIMITER)) {
// todo how to deserialize the string to a known type?
}
return list;
}
return context.readValue(p, javaType);
}
and i don't know how to deserialize the string to the known content type. Is there any way to implement a universal deserializer?
To avoid manual deserialisation and handling all possible types we can use a fact that all items on the list are also JSON elements when we wrap them with a quote (") char.
So, we can convert John, Tom to a "John", "Tom", 8, 9 to "8", "9" and so on.
We can use default Jackson behaviour which allows to handle unexpected tokens. In our case whenever: STRING token appears when JSON ARRAY was expected. To handle these cases we can use com.fasterxml.jackson.databind.deser.DeserializationProblemHandler class. It could look like below:
class ComaSeparatedValuesDeserializationProblemHandler extends DeserializationProblemHandler {
#Override
public Object handleUnexpectedToken(DeserializationContext ctxt, JavaType targetType, JsonToken token, JsonParser parser, String failureMsg) throws IOException {
if (token == JsonToken.VALUE_STRING && targetType.isCollectionLikeType()) {
return deserializeAsList(targetType, parser);
}
return super.handleUnexpectedToken(ctxt, targetType, token, parser, failureMsg);
}
private Object deserializeAsList(JavaType listType, JsonParser parser) throws IOException {
String[] values = readValues(parser);
ObjectMapper mapper = (ObjectMapper) parser.getCodec();
JavaType itemType = listType.getContentType();
List<Object> result = new ArrayList<>();
for (String value : values) {
result.add(convertToItemType(mapper, itemType, value));
}
return result;
}
private Object convertToItemType(ObjectMapper mapper, JavaType contentType, String value) throws IOException {
final String json = "\"" + value.trim() + "\"";
return mapper.readValue(json, contentType);
}
private String[] readValues(JsonParser p) throws IOException {
final String text = p.getText();
return text.split(",");
}
}
Example usage:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.google.common.base.Joiner;
import lombok.Data;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ConvertStringToCollectionApp {
public static void main(String[] args) throws IOException {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = JsonMapper.builder()
.addHandler(new ComaSeparatedValuesDeserializationProblemHandler())
.build();
Bean bean = mapper.readValue(jsonFile, Bean.class);
print(bean.getNames());
print(bean.getValues());
print(bean.getStatuses());
}
private static void print(List<?> values) {
values.stream().findFirst().ifPresent(value -> System.out.print(value.getClass().getSimpleName() + "s: "));
System.out.println(Joiner.on(", ").join(values));
}
}
#Data
class Bean {
private List<String> names;
private List<Integer> values;
private List<StatusEnum> statuses;
}
enum StatusEnum {
yes, no
}
Above app for your JSON payload prints:
Strings: John, Tom
Integers: 8, 9
StatusEnums: yes, no
I used Lombok and Guava libraries just to make it simple and short but they are not mandatory to make it work.
Your Bean doesn't correctly represents the JSON. The correct version should look something like below
class Bean {
private String names;
private Integer values;
private String statuses;
}
And you can use Object Mapper
ObjectMapper objectMapper = new ObjectMapper();
Bean bean = objectMapper.readValue(json, Bean.class);
Finally, you can break down your Bean object to list of names, values and status for your further usages.
I was asked to change our jackson mapping configuration so that each empty object we deserialize (from JSON) is going to be deserialized as null.
The problem is that I'm struggling to do it, but without any luck. Here is a sample of our ObjectMapper configuration (and example):
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, true);
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ISO_DATE_TIME));
javaTimeModule.addDeserializer(Instant.class, InstantDeserializer.INSTANT);
mapper.registerModule(javaTimeModule);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
warmupMapper(mapper);
return mapper;
I thought about something like adding:
mapper.configure(
DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true);
but it just works on strings.
I'm afraid that using a custom deserializer will not help me, because I'm writing a generic (for all objects) mapper. So I probably need something like a delegator or a post process deserialization method.
So for json like "" or {} I expect to be converted to null in java (and not to empty string or Object instance).
What is a empty object for you? A object with null value fields? A object with no fields? You can create a custom to check the nodes and deserialize how you want. I see no problem to use it in a generic way.
I did a little example:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.util.Objects;
public class DeserializerExample<T> extends StdDeserializer<T> {
private final ObjectMapper defaultMapper;
public DeserializerExample(Class<T> clazz) {
super(clazz);
defaultMapper = new ObjectMapper();
}
#Override
public T deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
System.out.println("Deserializing...");
JsonNode node = jp.getCodec().readTree(jp);
for (JsonNode jsonNode : node) {
if (!jsonNode.isNull()) {
return defaultMapper.treeToValue(node, (Class<T>) getValueClass());
}
}
return null;
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Person.class, new DeserializerExample(Person.class));
mapper.registerModule(module);
Person person = mapper.readValue("{\"id\":1, \"name\":\"Joseph\"}", Person.class);
Person nullPerson = mapper.readValue("{\"id\":null, \"name\":null}", Person.class);
System.out.println("Is null: " + Objects.isNull(person));
System.out.println("Is null: " + Objects.isNull(nullPerson));
}
}
The only way to do this is to use a custom deserializer:
class CustomDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.readValueAsTree();
if (node.asText().isEmpty()) {
return null;
}
return node.toString();
}
}
Then do:
class EventBean {
public Long eventId;
public String title;
#JsonDeserialize(using = CustomDeserializer.class)
public String location;
}
This solution courtesy of Sach141 on this question.
I had the same problem.
I hava a City class and sometimes I recive 'city':{} from a web service request.
So, the standard serializer create a new City with all empty field.
I created a custom deserializer in this way
public class CityJsonDeSerializer extends StdDeserializer<City> {
#Override
public City deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
if(node.isNull() || node.asText().isEmpty()|| node.size()==0)
return null;
City city = new City();
... // set all fields
return city;
}
}
The if check the conditions:
'city' : null
'city' : ''
'city' : '{}'
and if it's true, the deserializer returns null.
Another approach is to use a com.fasterxml.jackson.databind.util.Converter<IN,OUT>, which is essentially a postprocessor for deserialization.
Imagine we have a class:
public class Person {
public String id;
public String name;
}
Now imagine we want to deserialize an empty JSON object {} as null, rather than a Person with null values for id and name. We can create the following Converter:
public PersonConverter implements Converter<Person,Person> {
#Override
public Person convert(Person p) {
return isEmpty(p) ? null : value;
}
private static boolean isEmpty(Person p) {
if(p == null) {
return true;
}
if(Optional.ofNullable(p.id).orElse("").isEmpty() &&
Optional.ofNullable(p.name).orElse("").isEmpty()) {
return true;
}
return false;
}
#Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(Person.class);
}
#Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(Person.class);
}
}
Note that we have to handle the blank String case because that is (counter-intuitively) the default value assigned to properties not given in JSON, as opposed to null.
Given the converter, we can then annotate our original Person class:
#JsonDeserialize(converter=PersonConverter.class)
public class Person {
public String id;
public String name;
}
The benefit of this approach is that you don't have to think or care about deserialization at all; you're simply deciding what to do with the deserialized object after it's deserialized. And there are many other transformations you can do with a Converter, too. But this works nicely for nullifying "empty" values.
I need some help. I must get next json:
{
"433434" : {
"type" : "MULTIPLE",
"value" : [ {
"type" : "NUMBER",
"value" : 322332
}, {
"type" : "NUMBER",
"value" : 322332
} ]
}
}
But I have this:
{
"433434" : {
"value" : [ {
"type" : "NUMBER",
"value" : 322332
}, {
"type" : "NUMBER",
"value" : 322332
} ]
}
}
I am using Jackson. Its my main class
package com.un1acker;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.un1acker.characteristic.AbstractCharacteristic;
import com.un1acker.characteristic.MultipleCharacteristic;
import com.un1acker.characteristic.NumCharacteristic;
import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Main {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
NumCharacteristic numCharacteristic = new NumCharacteristic();
numCharacteristic.setValue(BigInteger.valueOf(322332L));
List<AbstractCharacteristic<?>> list = new ArrayList<>();
list.add(numCharacteristic);
list.add(numCharacteristic);
StringWriter sw = new StringWriter();
MultipleCharacteristic multipleCharacteristic = new MultipleCharacteristic();
multipleCharacteristic.setValue(list);
Map<String, AbstractCharacteristic<?>> map = new HashMap<>();
map.put("433434", multipleCharacteristic);
mapper.writeValue(sw, map);
System.out.println(sw.toString());
}
}
And I have class MultipleCharateristic and NumberCharacteristic who extends from AbstractCharacteristic. Planned that we have map>, that contains in value MultipleCharacteristic.
In the MultipleCharacteristic set list of NumberCharacteristic values.
package com.un1acker.characteristic;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
#JsonSubTypes({ #JsonSubTypes.Type(
value = NumCharacteristic.class,
name = "NUMBER"
), #JsonSubTypes.Type(
value = MultipleCharacteristic.class,
name = "MULTIPLE")})
public abstract class AbstractCharacteristic<T>{
private static final long serialVersionUID = -6524899961842198462L;
private T value;
public AbstractCharacteristic() {
}
public T getValue() {
return this.value;
}
protected void setValue(T value) {
this.value = value;
}
}
NumCharacteristic Class
package com.un1acker.characteristic;
import java.math.BigInteger;
public class NumCharacteristic extends AbstractCharacteristic<BigInteger> {
private static final long serialVersionUID = 9220460768952701281L;
public NumCharacteristic() {
}
public void setValue(BigInteger value) {
super.setValue(value);
}
}
MultipleCharacteristic class
package com.un1acker.characteristic;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.un1acker.MyCustomSerializer;
import java.util.List;
public class MultipleCharacteristic extends AbstractCharacteristic<List<? extends AbstractCharacteristic<?>>> {
#Override
public void setValue(List<? extends AbstractCharacteristic<?>> value) {
super.setValue(value);
}
#Override
#JsonSerialize(using = MyCustomSerializer.class)
public List<? extends AbstractCharacteristic<?>> getValue() {
return super.getValue();
}
}
I tried create custom serialize for the MultipleClass with override method serializeWithType but this doesnt work.
This looks like it might be a Jackson bug. Your MultipleCharacteristic type serializes fine and includes the type when on its own, but not when it's a map value, which I would expect to work the same way:
#Test // passes
public void serialize_multiple_characteristic() throws Exception {
MultipleCharacteristic chr = new MultipleCharacteristic();
chr.setValue(new ArrayList<>());
ObjectMapper mapper = new ObjectMapper();
assertThat(mapper.writeValueAsString(chr), equivalentTo("{ type: 'MULTIPLE', value: [] }"));
}
#Test // fails, got: {"xyzzy":{"value":[]}}
public void serialize_multiple_characteristic_in_map_value() throws Exception {
MultipleCharacteristic chr = new MultipleCharacteristic();
chr.setValue(new ArrayList<>());
ObjectMapper mapper = new ObjectMapper();
Map<String, MultipleCharacteristic> map = new HashMap<>();
map.put("xyzzy", chr);
assertThat(mapper.writeValueAsString(map), equivalentTo("{ 'xyzzy': { type: 'MULTIPLE', value: [] } }"));
}
(looking through the innards, the BeanSerializer never gets wrapped in TypeWrappedSerializer)
This might not be a problem for you if the map is embedded in something as, as if Jackson knows the type parameters for the map (which it would normally get from a containing bean, for example) it seems to do the right thing:
#Test // passes
public void serialize_multiple_characteristic_in_map_value_using_writer() throws Exception {
MultipleCharacteristic chr = new MultipleCharacteristic();
chr.setValue(new ArrayList<>());
ObjectMapper mapper = new ObjectMapper();
Map<String, AbstractCharacteristic<?>> map = new HashMap<>();
map.put("xyzzy", chr);
// Hint to Jackson what types will be in the map
TypeReference<?> mapType = new TypeReference<Map<String, AbstractCharacteristic<?>>>(){};
assertThat(mapper.writerFor(mapType).writeValueAsString(map), equivalentTo("{ 'xyzzy': { type: 'MULTIPLE', value: [] } }"));
}
One workaround, if you just need to get the JSON produced and can't change the code to use ObjectWrite, is to use ObjectNode instead of Map:
#Test
public void serialize_multiple_characteristic_in_json_node() throws Exception {
MultipleCharacteristic chr = new MultipleCharacteristic();
chr.setValue(new ArrayList<>());
ObjectMapper mapper = new ObjectMapper();
ObjectNode node = mapper.getNodeFactory().objectNode();
node.putPOJO("xyzzy", chr);
assertThat(mapper.writeValueAsString(node), equivalentTo("{ 'xyzzy': { type: 'MULTIPLE', value: [] } }"));
}
This works for me: Add custom serializer for MultipleCharacteristic:
public class MultipleCharacteristicValueSerializer extends JsonSerializer<MultipleCharacteristic> {
#Override
public void serialize(MultipleCharacteristic multipleCharacteristicValue, JsonGenerator jsonGenerator,SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeFieldName("value");
jsonGenerator.writeStartArray();
for (AbstractCharacteristic<?> characteristicValue : multipleCharacteristicValue.getValue()) {
jsonGenerator.writeObject(characteristicValue);
}
jsonGenerator.writeEndArray();
}
#Override
public void serializeWithType(MultipleCharacteristic value, JsonGenerator jgen, SerializerProvider provider, TypeSerializer typeSer) throws IOException, JsonProcessingException {
typeSer.writeTypePrefixForObject(value, jgen);
serialize(value, jgen, provider);
typeSer.writeTypeSuffixForObject(value, jgen);
}
#Override
public Class<MultipleCharacteristic> handledType() {
return MultipleCharacteristic.class;
}
}
I have a result from a web service that returns either a boolean value or a singleton map, e.g.
Boolean result:
{
id: 24428,
rated: false
}
Map result:
{
id: 78,
rated: {
value: 10
}
}
Individually I can map both of these easily, but how do I do it generically?
Basically I want to map it to a class like:
public class Rating {
private int id;
private int rated;
...
public void setRated(?) {
// if value == false, set rated = -1;
// else decode "value" as rated
}
}
All of the polymorphic examples use #JsonTypeInfo to map based on a property in the data, but I don't have that option in this case.
EDIT
The updated section of code:
#JsonProperty("rated")
public void setRating(JsonNode ratedNode) {
JsonNode valueNode = ratedNode.get("value");
// if the node doesn't exist then it's the boolean value
if (valueNode == null) {
// Use a default value
this.rating = -1;
} else {
// Convert the value to an integer
this.rating = valueNode.asInt();
}
}
No no no. You do NOT have to write a custom deserializer. Just use "untyped" mapping first:
public class Response {
public long id;
public Object rated;
}
// OR
public class Response {
public long id;
public JsonNode rated;
}
Response r = mapper.readValue(source, Response.class);
which gives value of Boolean or java.util.Map for "rated" (with first approach); or a JsonNode in second case.
From that, you can either access data as is, or, perhaps more interestingly, convert to actual value:
if (r.rated instanceof Boolean) {
// handle that
} else {
ActualRated actual = mapper.convertValue(r.rated, ActualRated.class);
}
// or, if you used JsonNode, use "mapper.treeToValue(ActualRated.class)
There are other kinds of approaches too -- using creator "ActualRated(boolean)", to let instance constructed either from POJO, or from scalar. But I think above should work.
You have to write your own deserializer. It could look like this:
#SuppressWarnings("unchecked")
class RatingJsonDeserializer extends JsonDeserializer<Rating> {
#Override
public Rating deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Map<String, Object> map = jp.readValueAs(Map.class);
Rating rating = new Rating();
rating.setId(getInt(map, "id"));
rating.setRated(getRated(map));
return rating;
}
private int getInt(Map<String, Object> map, String propertyName) {
Object object = map.get(propertyName);
if (object instanceof Number) {
return ((Number) object).intValue();
}
return 0;
}
private int getRated(Map<String, Object> map) {
Object object = map.get("rated");
if (object instanceof Boolean) {
if (((Boolean) object).booleanValue()) {
return 0; // or throw exception
}
return -1;
}
if (object instanceof Map) {
return getInt(((Map<String, Object>) object), "value");
}
return 0;
}
}
Now you have to tell Jackson to use this deserializer for Rating class:
#JsonDeserialize(using = RatingJsonDeserializer.class)
class Rating {
...
}
Simple usage:
ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.readValue(json, Rating.class));
Above program prints:
Rating [id=78, rated=10]
for JSON:
{
"id": 78,
"rated": {
"value": 10
}
}
and prints:
Rating [id=78, rated=-1]
for JSON:
{
"id": 78,
"rated": false
}
I found a nice article on the subject: http://programmerbruce.blogspot.com/2011/05/deserialize-json-with-jackson-into.html
I think that the approach of parsing into object, is possibly problematic, because when you send it, you send a string. I am not sure it is an actual issue, but it sounds like some possible unexpected behavior.
example 5 and 6 show that you can use inheritance for this.
Example:
Example 6: Simple Deserialization Without Type Element To Container Object With Polymorphic Collection
Some real-world JSON APIs have polymorphic type members, but don't include type elements (unlike the JSON in the previous examples). Deserializing such sources into polymorphic collections is a bit more involved. Following is one relatively simple solution. (This example includes subsequent serialization of the deserialized Java structure back to input JSON, but the serialization is relatively uninteresting.)
// input and output:
// {
// "animals":
// [
// {"name":"Spike","breed":"mutt","leash_color":"red"},
// {"name":"Fluffy","favorite_toy":"spider ring"},
// {"name":"Baldy","wing_span":"6 feet",
// "preferred_food":"wild salmon"}
// ]
// }
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.deser.StdDeserializer;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.node.ObjectNode;
import fubar.CamelCaseNamingStrategy;
public class Foo
{
public static void main(String[] args) throws Exception
{
AnimalDeserializer deserializer =
new AnimalDeserializer();
deserializer.registerAnimal("leash_color", Dog.class);
deserializer.registerAnimal("favorite_toy", Cat.class);
deserializer.registerAnimal("wing_span", Bird.class);
SimpleModule module =
new SimpleModule("PolymorphicAnimalDeserializerModule",
new Version(1, 0, 0, null));
module.addDeserializer(Animal.class, deserializer);
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(
new CamelCaseNamingStrategy());
mapper.registerModule(module);
Zoo zoo =
mapper.readValue(new File("input_6.json"), Zoo.class);
System.out.println(mapper.writeValueAsString(zoo));
}
}
class AnimalDeserializer extends StdDeserializer<Animal>
{
private Map<String, Class<? extends Animal>> registry =
new HashMap<String, Class<? extends Animal>>();
AnimalDeserializer()
{
super(Animal.class);
}
void registerAnimal(String uniqueAttribute,
Class<? extends Animal> animalClass)
{
registry.put(uniqueAttribute, animalClass);
}
#Override
public Animal deserialize(
JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
Class<? extends Animal> animalClass = null;
Iterator<Entry<String, JsonNode>> elementsIterator =
root.getFields();
while (elementsIterator.hasNext())
{
Entry<String, JsonNode> element=elementsIterator.next();
String name = element.getKey();
if (registry.containsKey(name))
{
animalClass = registry.get(name);
break;
}
}
if (animalClass == null) return null;
return mapper.readValue(root, animalClass);
}
}
class Zoo
{
public Collection<Animal> animals;
}
abstract class Animal
{
public String name;
}
class Dog extends Animal
{
public String breed;
public String leashColor;
}
class Cat extends Animal
{
public String favoriteToy;
}
class Bird extends Animal
{
public String wingSpan;
public String preferredFood;
}
I asked a similar question - JSON POJO consumer of polymorphic objects
You have to write your own deserialiser that gets a look-in during the deserialise process and decides what to do depending on the data.
There may be other easier methods but this method worked well for me.
Would it be possible if someone could help me parse this json result. I have retrieved the result as a string
{"query":{"latitude":39.9889,"longitude":-82.8118},"timestamp":1310252291.861,"address":{"geometry":{"coordinates":[-82.81168367358264,39.9887910986731],"type":"Point"},"properties":{"address":"284 Macdougal Ln","distance":"0.02","postcode":"43004","city":"Columbus","county":"Franklin","province":"OH","country":"US"},"type":"Feature"}}
Jackson. Simple and intuitive to use. For me the best available. Start out with Simple Data Binding, it will throw everything it finds in Maps and Lists.
Like this:
ObjectMapper mapper = new ObjectMapper();
Map<String,Object> yourData = mapper.readValue(new File("yourdata.json"), Map.class);
That's all that's needed.
A good and quick introduction can be found here
And a full working example with your actual data:
import java.io.IOException;
import java.util.Map;
import org.codehaus.jackson.map.ObjectMapper;
public class Main {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Map<?,?> rootAsMap = mapper.readValue(
"{\"query\":{\"latitude\":39.9889,\"longitude\":-82.8118},\"timestamp\":1310252291.861,\"address\":{\"geometry\":{\"coordinates\":[-82.81168367358264,39.9887910986731],\"type\":\"Point\"},\"properties\":{\"address\":\"284 Macdougal Ln\",\"distance\":\"0.02\",\"postcode\":\"43004\",\"city\":\"Columbus\",\"county\":\"Franklin\",\"province\":\"OH\",\"country\":\"US\"},\"type\":\"Feature\"}}".getBytes(),
Map.class);
System.out.println(rootAsMap);
Map query = (Map) rootAsMap.get("query");
Map address = (Map) rootAsMap.get("address");
Map addressProperties = (Map) address.get("properties");
String county = (String) addressProperties.get("county");
System.out.println("County is " + county);
}
}
Now, this whole Map juggling also illustrates Bozho's point pretty well, using full binding (by creating a Java class that reflects the content of the JSON data) will work better in the end.
The two best options that I know of are:
Jackson
gson
Using them is a matter of calling one method of the mapper. But remember that since Java is statically-typed, you may have to create an object that has the required structure. (You don't have to, but it feels more natural)
From http://www.json.org, under the Java section:
http://www.json.org/java/index.html
http://json-lib.sourceforge.net/
http://code.google.com/p/json-simple/
http://code.google.com/p/jjson/
Pick your poison
With Jackson, following is the approach I'd take. Since the coordinates in the JSON come in two different formats -- sometimes an object, sometimes an array -- the solution is mildly complicated with necessary custom deserialization processing.
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParser;
import org.codehaus.jackson.JsonProcessingException;
import org.codehaus.jackson.ObjectCodec;
import org.codehaus.jackson.Version;
import org.codehaus.jackson.annotate.JsonAutoDetect.Visibility;
import org.codehaus.jackson.map.DeserializationContext;
import org.codehaus.jackson.map.JsonDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.node.ArrayNode;
public class JacksonFoo
{
public static void main(String[] args) throws Exception
{
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibilityChecker(mapper.getVisibilityChecker().withFieldVisibility(Visibility.ANY));
mapper.registerModule(
new SimpleModule("CoordinatesDeserializer", Version.unknownVersion())
.addDeserializer(Coordinates.class, new CoordinatesDeserializer()));
Result result = mapper.readValue(new File("input.json"), Result.class);
System.out.println(mapper.writeValueAsString(result));
}
}
class CoordinatesDeserializer extends JsonDeserializer<Coordinates>
{
#Override
public Coordinates deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException
{
ObjectCodec codec = jp.getCodec();
JsonNode node = codec.readTree(jp);
if (node.isObject())
{
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibilityChecker(mapper.getVisibilityChecker().withFieldVisibility(Visibility.ANY));
return mapper.readValue(node, Coordinates.class);
}
// else it's an array
ArrayNode array = (ArrayNode) node;
Coordinates coordinates = new Coordinates();
coordinates.latitude = codec.treeToValue(array.get(0), BigDecimal.class);
coordinates.latitude = codec.treeToValue(array.get(1), BigDecimal.class);
return coordinates;
}
}
class Result
{
Coordinates query;
BigDecimal timestamp;
Address address;
}
class Coordinates
{
BigDecimal latitude;
BigDecimal longitude;
}
class Address
{
String type;
Geometry geometry;
AddressDetails properties;
}
class Geometry
{
String type;
Coordinates coordinates;
}
class AddressDetails
{
String address;
BigDecimal distance;
String postcode;
String city;
String county;
String province;
String country;
}