I have the following class
public class BetWrapper {
private String description;
private Calendar startTime;
private HashMap<String, SimpleEntry<Integer, Double>> map;
public BetWrapper() {
map = new HashMap<>();
}
public Calendar getStartTime() {
return startTime;
}
public void setStartTime(Calendar startTime) {
this.startTime = startTime;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public HashMap<String, SimpleEntry<Integer, Double>> getMap() {
return map;
}
public void setMap(HashMap<String, SimpleEntry<Integer, Double>> map) {
this.map = map;
}
}
And I'm using JSONUtil class
public class JSONUtil {
private JSONUtil() {}
public static <T> T fromJSON(String content, Class<T> clazz) throws TechnicalException {
try {
return new ObjectMapper().readValue(content, clazz);
} catch (IOException e) {
throw new TechnicalException(e);
}
}
public static String toJSON(Object obj) throws TechnicalException {
try {
return new ObjectMapper().writeValueAsString(obj);
} catch (JsonProcessingException ex) {
throw new TechnicalException(ex);
}
}
}
I want to deserialzize a JSON to an BetWrapper object. But the following code produces some exceptions.
BetWrapper betWrapper = new BetWrapper();
betWrapper.setDescription("Stoke City - Arsenal");
betWrapper.setStartTime(Calendar.getInstance());
HashMap<String, AbstractMap.SimpleEntry<Integer, Double>> map = new HashMap<>();
map.put("home_team", new AbstractMap.SimpleEntry<>(1, 2.85));
betWrapper.setMap(map);
String json = JSONUtil.toJSON(betWrapper);
JSONUtil.fromJSON(json, BetWrapper.class);
The exceptions are:
Caused by: com.fasterxml.jackson.databind.JsonMappingException: No suitable constructor found for type [simple type, class java.util.AbstractMap$SimpleEntry<java.lang.Integer,java.lang.Double>]: can not instantiate from JSON object (need to add/enable type information?)
at [Source: {"description":"Stoke City - Arsenal","startTime":1417648132139,"map":{"home_team":"key":1,"value":2.85}}}; line: 1, column: 85] (through reference chain: by.bsu.kolodyuk.bettingapp.model.entity.BetWrapper["map"])
How to deserialize it correctly? It seems like that the problem is that types K,V in SimpleEntry should be specified for Jackson in some way.
Any Ideas?
The type SimpleEntry has the following constructor
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
By default, Jackson expects a parameterless constructor. If such a constructor doesn't exist, it looks for one with #JsonProperty annotations. (I might have this backwards, but don't ever code like that.) Since you're working with a JDK type, it obviously won't have those annotations.
You can use Mixins. Jackson uses these as templates to deserialize your target type.
abstract class Mixin<K, V> {
public Mixin(#JsonProperty("key") K key, #JsonProperty("value") V value) {}
}
...
public static <T> T fromJSON(String content, Class<T> clazz) throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.addMixInAnnotations(SimpleEntry.class, Mixin.class);
return mapper.readValue(content, clazz);
}
Jackson will use the meta type Mixin, to deserialize to SimpleEntry objects.
(Note, the type parameters of Mixin and the constructor parameter types don't really matter. It's the fact that there are two constructor parameters and that the constructor parameters are annotated that matters.)
In Jackson, you can define custom deserializers. So for your case, it may look like this:
import java.io.IOException;
import java.util.AbstractMap;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
public class SomeDeserializer extends StdDeserializer<AbstractMap.SimpleEntry>
{
public SomeDeserializer()
{
super( AbstractMap.SimpleEntry.class );
}
#Override
public AbstractMap.SimpleEntry deserialize( JsonParser jp, DeserializationContext ctxt ) throws IOException, JsonProcessingException
{
Integer key = null;
Double value = null;
JsonToken token;
while ( ( token = jp.nextValue() ) != null )
{
if ( token.isNumeric() )
{
String propertyName = jp.getCurrentName();
if ( "key".equalsIgnoreCase( propertyName ) )
{
key = jp.getIntValue();
}
else if ( "value".equalsIgnoreCase( propertyName ) )
{
value = jp.getDoubleValue();
}
}
}
if ( key != null && value != null )
{
return new AbstractMap.SimpleEntry( key, value );
}
return null;
}
}
Deserializers should be registered using ObjectMapper.registerModule(Module m). In your case, you can do it in your JSONUtil utility class:
SimpleModule module = new SimpleModule();
module.addDeserializer( AbstractMap.SimpleEntry.class, new SomeDeserializer() );
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule( module );
Note that deserializer is registered with ObjectMapper instance. Thus you better store the instance as a field of your utility class.
The deserializer class above is not comprehensive! This is just for demonstration for the case in hand. Further optimizations and refactorings can be applied as needed.
Related
We are using SpringDataMongoDB in a Spring-boot app to manage our data.
Our previous model was this:
public class Response implements Serializable {
//...
private JsonNode errorBody; //<-- Dynamic
//...
}
JsonNode FQDN is com.fasterxml.jackson.databind.JsonNode
Which saved documents like so in the DB:
"response": {
...
"errorBody": {
"_children": {
"code": {
"_value": "Error-code-value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
},
"message": {
"_value": "Error message value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
},
"description": {
"_value": "Error description value",
"_class": "com.fasterxml.jackson.databind.node.TextNode"
}
},
"_nodeFactory": {
"_cfgBigDecimalExact": false
},
"_class": "com.fasterxml.jackson.databind.node.ObjectNode"
},
...
}
We've saved hundreds of documents like this on the production database without ever the need to read them programmatically as they are just kind of logs.
As we noticed that this output could be difficult to read in the future, we've decided to change the model to this:
public class Response implements Serializable {
//...
private Map<String,Object> errorBody;
//...
}
The data are now saved like so:
"response": {
...
"errorBody": {
"code": "Error code value",
"message": "Error message value",
"description": "Error description value",
...
},
...
}
Which, as you may have noticed is pretty much more simple.
When reading the data, ex: repository.findAll()
The new format is read without any issue.
But we face these issues with the old format:
org.springframework.data.mapping.MappingException: No property v found on entity class com.fasterxml.jackson.databind.node.TextNode to bind constructor parameter to!
Or
org.springframework.data.mapping.model.MappingInstantiationException: Failed to instantiate com.fasterxml.jackson.databind.node.ObjectNode using constructor NO_CONSTRUCTOR with arguments
Of course the TextNode class has a constructor with v as param but the property name is _value and ObjectNode has no default constructor: We simply can't change that.
We've created custom converters that we've added to our configurations.
public class ObjectNodeWriteConverter implements Converter<ObjectNode, DBObject> {
#Override
public DBObject convert(ObjectNode source) {
return BasicDBObject.parse(source.toString());
}
}
public class ObjectNodeReadConverter implements Converter<DBObject, ObjectNode> {
#Override
public ObjectNode convert(DBObject source) {
try {
return new ObjectMapper().readValue(source.toString(), ObjectNode.class);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
We did the same for TextNode
But we still got the errors.
The converters are read as we have a ZonedDateTimeConverter that is doing his job.
We can not just wipe out or ignore the old data as we need to read them too in order to study them.
How can we set up a custom reader that will not fail reading the old format ?
As I understood your issue, with the first model, you didn't really have a problem to save or to read in database but, once you wanted to fetch these datas, you noticed that the output is difficult to read. So your problem is to fetch a well readable output then you don't need to change the first model but to extends these classes and overide the toString method to change its behavior while fetching.
There are at least three classes to extends:
TextNode : you can't overide the toString method do that the custom class just print the value
ObjectNode : I can see that there are at least four field inside this class that you want to fecth the value: code, message, description. They are type of TextNode so you can replace them by thier extended classes. Then overide the toString method so that It print fieldName: field.toString() for each field
JsonNode : You can then extend this class and use the custom classes created above, overide the toString method so that It print as you want and use It instead of the common JsonNode
To work like that will make you avoid the way you save or you read the datas but just to fecth on the view.
You can consider it as a little part of the SOLID principle especially the OCP (Open an close principle: avoid to change the class behavoir but extends it to create a custom behavior) and the LSP (Liskov Substitution Principle: Subtypes must be behaviorlly substituable for thier base types).
Since old format is predefined and you know a structure of it you can implement custom deserialiser to handle old and new format at the same time. If errorBody JSON Object contains any of these keys: _children, _nodeFactory or _class you know it is an old format and you need to iterate over keys in _children JSON Object and get _value key to find a real value. Rest of keys and values you can ignore. Simple implementation could look like below:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.TreeNode;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.Data;
import lombok.ToString;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class JsonMongo2FormatsApp {
public static void main(String[] args) throws IOException {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
JsonMapper mapper = JsonMapper.builder().build();
Response response = mapper.readValue(jsonFile, Response.class);
System.out.println(response.getErrorBody());
}
}
#Data
#ToString
class Response {
#JsonDeserialize(using = ErrorMapJsonDeserializer.class)
private Map<String, String> errorBody;
}
class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, String>> {
#Override
public Map<String, String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
if (!root.isObject()) {
// ignore everything except JSON Object
return Collections.emptyMap();
}
ObjectNode objectNode = (ObjectNode) root;
if (isOldFormat(objectNode)) {
return deserialize(objectNode);
}
return toMap(objectNode);
}
protected boolean isOldFormat(ObjectNode objectNode) {
final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
final Iterator<String> iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map<String, String> deserialize(ObjectNode root) {
JsonNode children = root.get("_children");
Map<String, String> result = new LinkedHashMap<>();
children.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().get("_value").toString());
});
return result;
}
private Map<String, String> toMap(ObjectNode objectNode) {
Map<String, String> result = new LinkedHashMap<>();
objectNode.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}
Above deserialiser should handle both formats.
Michal Ziober's answer did not completely solve the problem as we need to tell SpringData MongoDb that we want it to use the custom deserializer
(Annotating the model does not work with Spring data mongodb):
Define the custom deserializer
public class ErrorMapJsonDeserializer extends JsonDeserializer<Map<String, Object>> {
#Override
public Map<String, Object> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
TreeNode root = p.readValueAsTree();
if (!root.isObject()) {
// ignore everything except JSON Object
return Collections.emptyMap();
}
ObjectNode objectNode = (ObjectNode) root;
if (isOldFormat(objectNode)) {
return deserialize(objectNode);
}
return toMap(objectNode);
}
protected boolean isOldFormat(ObjectNode objectNode) {
final List<String> oldFormatKeys = Arrays.asList("_children", "_nodeFactory", "_class");
final Iterator<String> iterator = objectNode.fieldNames();
while (iterator.hasNext()) {
String field = iterator.next();
return oldFormatKeys.contains(field);
}
return false;
}
protected Map<String, Object> deserialize(ObjectNode root) {
JsonNode children = root.get("_children");
if (children.isArray()) {
children = children.get(0);
children = children.get("_children");
}
return extractValues(children);
}
private Map<String, Object> extractValues(JsonNode children) {
Map<String, Object> result = new LinkedHashMap<>();
children.fields().forEachRemaining(entry -> {
String key = entry.getKey();
if (!key.equals("_class"))
result.put(key, entry.getValue().get("_value").toString());
});
return result;
}
private Map<String, Object> toMap(ObjectNode objectNode) {
Map<String, Object> result = new LinkedHashMap<>();
objectNode.fields().forEachRemaining(entry -> {
result.put(entry.getKey(), entry.getValue().toString());
});
return result;
}
}
Create a Custom mongo converter and pass it the custom deserializer.
Actually we do not pass the serializer directly but by means of an ObjectMapper configured with that Custom deserializer
public class CustomMappingMongoConverter extends MappingMongoConverter {
//The configured objectMapper that will be passed during instantiation
private ObjectMapper objectMapper;
public CustomMappingMongoConverter(DbRefResolver dbRefResolver, MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext, ObjectMapper objectMapper) {
super(dbRefResolver, mappingContext);
this.objectMapper = objectMapper;
}
#Override
public <S> S read(Class<S> clazz, Bson dbObject) {
try {
return objectMapper.readValue(dbObject.toString(), clazz);
} catch (IOException e) {
throw new RuntimeException(dbObject.toString(), e);
}
}
//in case you want to serialize with your custom objectMapper as well
#Override
public void write(Object obj, Bson dbo) {
String string = null;
try {
string = objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(string, e);
}
((DBObject) dbo).putAll((DBObject) BasicDBObject.parse(string));
}
}
Create and configure the object mapper then instantiate the custom MongoMappingConverter and add it to Mongo configurations
public class MongoConfiguration extends AbstractMongoClientConfiguration {
//... other configuration method beans
#Bean
#Override
public MappingMongoConverter mappingMongoConverter() throws Exception {
DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.registerModule(new SimpleModule() {
{
addDeserializer(Map.class, new ErrorMapJsonDeserializer());
}
});
return new CustomMappingMongoConverter(dbRefResolver, mongoMappingContext(), objectMapper);
}
}
I need to write a method that takes some object, some field name fieldName that exists in the given object's class, and some field value value. The value is the JSON-serialized form of the field. That method shall take the value and deserialize it accordingly, something like this:
static void setField(Object obj, String fieldName, String value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName)
Object valObj = objectMapper.readValue(value, field.getType());
field.set(obj, valObj);
}
(I actually only need to retrieve the deserialized value, and not set it again, but this makes it a better example.)
This works, as long as jackson's default deserialization is sufficient. Now let's assume I have a class with a custom (de)serializer:
class SomeDTO {
String foo;
#JsonSerialize(using = CustomInstantSerializer.class)
#JsonDeserialize(using = CustomInstantDeserializer.class)
Instant bar;
}
One possible solution would be to manually check for JsonDeserialize annotations. However, I really do not want to try to replicate whatever policies Jackson follows to decide what serializer to use, as that seems brittle (for example globally registered serializers).
Is there a good way to deserialize the value using the field's deserialization configuration defined in the DTO class? Maybe deserializing the value into the field's type while passing the field's annotations along to Jackson, so they get honored?
I managed to get a hold of an AnnotatedMember instance, which holds all the required information (JSON-annotations and reflective field- or setter/getter-access), but couldn't figure out how I would use it to deserialize a standalone value due to lack of documentation:
final JavaType dtoType = objectMapper.getTypeFactory().constructType(SomeDTO.class);
final BeanDescription description = objectMapper.getDeserializationConfig().introspect(dtoType);
for (BeanPropertyDefinition propDef: beanDescription.findProperties()) {
final AnnotatedMember mutator = propertyDefinition.getNonConstructorMutator();
// now what? Also: How do I filter for the correct property?
}
One possibility would be to serialize the object, replace the given field, and then deserialize it again. This can be easily done when serializing from/to JsonNode instead of JSON-String, like this:
static Object setField(Object obj, String fieldName, String value) throws Exception {
// note: produces a new object instead of modifying the existing one
JsonNode node = objectMapper.valueToTree(obj);
((ObjectNode) node).put(fieldName, value);
return objectMapper.readValue(node.traverse(), obj.getClass());
}
However, serializing and deserializing a whole object just to deserialize a single field seems like a lot of overhead, and might be brittle because other aspects of the DTO class affect the deserialization process of the single field
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import java.io.IOException;
import java.util.Map;
public final class Jackson {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
public static void main(String[] args) throws IOException {
Dto source = makeDto("Master", 31337);
Dto dst = makeDto("Slave", 0xDEADBEEF);
//1. read value of field "fieldName" from json source
//2. clones destination object, sets up field "fieldName" and returns it
//3. in case of no field either on "src" or "dst" - throws an exception
Object result = restoreValue(dst, "details", OBJECT_MAPPER.writeValueAsString(source));
System.out.println(result);
}
private static Object restoreValue(Object targetObject, String fieldName, String sourceObjectAsJson) throws IOException {
String targetObjectAsJson = OBJECT_MAPPER.writeValueAsString(targetObject);
Map sourceAsMap = OBJECT_MAPPER.readValue(sourceObjectAsJson, Map.class);
Map targetAsMap = OBJECT_MAPPER.readValue(targetObjectAsJson, Map.class);
targetAsMap.put(fieldName, sourceAsMap.get(fieldName));
String updatedTargetAsJson = OBJECT_MAPPER.writeValueAsString(targetAsMap);
return OBJECT_MAPPER.readValue(updatedTargetAsJson, targetObject.getClass());
}
private static Dto makeDto(String name, int magic) {
Dto dto = new Dto();
dto.setName(name);
CustomDetails details = new CustomDetails();
details.setMagic(magic);
dto.setDetails(details);
return dto;
}
private static final class Dto {
private String name;
#JsonSerialize(using = CustomDetails.CustomDetailsSerializer.class)
#JsonDeserialize(using = CustomDetails.CustomDetailsDeserializer.class)
private CustomDetails details;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public CustomDetails getDetails() {
return details;
}
public void setDetails(CustomDetails details) {
this.details = details;
}
#Override
public String toString() {
return "Dto{" +
"name='" + name + '\'' +
", details=" + details +
'}';
}
}
private static final class CustomDetails {
private int magic;
public int getMagic() {
return magic;
}
public void setMagic(int magic) {
this.magic = magic;
}
#Override
public String toString() {
return "CustomDetails{" +
"magic=" + magic +
'}';
}
public static final class CustomDetailsSerializer extends StdSerializer<CustomDetails> {
public CustomDetailsSerializer() {
this(null);
}
public CustomDetailsSerializer(Class<CustomDetails> t) {
super(t);
}
#Override
public void serialize(CustomDetails details, JsonGenerator jg, SerializerProvider serializerProvider) throws IOException {
jg.writeStartObject();
jg.writeNumberField("_custom_property_magic", details.magic);
jg.writeEndObject();
}
}
private static final class CustomDetailsDeserializer extends StdDeserializer<CustomDetails> {
public CustomDetailsDeserializer() {
this(null);
}
public CustomDetailsDeserializer(Class<CustomDetails> t) {
super(t);
}
#Override
public CustomDetails deserialize(JsonParser jp, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
int magic = (Integer) node.get("_custom_property_magic").numberValue();
CustomDetails
customDetails = new CustomDetails();
customDetails.setMagic(magic);
return customDetails;
}
}
}
}
so the output is:
Dto{name='Slave', details=CustomDetails{magic=31337}}
I'm consuming a web service using Spring's RestTemplate and deserializing with Jackson.
In my JSON response from the server, one of the fields can be either an object or a list. meaning it can be either "result": [{}] or "result": {}.
Is there a way to handle this kind of things by annotations on the type I'm deserializing to ? define the member as an array[] or List<> and insert a single object in case of the second example ?
Can I write a new HttpMessageConverter that will handle it ?
Since you are using Jackson I think what you need is JsonDeserializer class (javadoc).
You can implement it like this:
public class ListOrObjectGenericJsonDeserializer<T> extends JsonDeserializer<List<T>> {
private final Class<T> cls;
public ListOrObjectGenericJsonDeserializer() {
final ParameterizedType type = (ParameterizedType) this.getClass().getGenericSuperclass();
this.cls = (Class<T>) type.getActualTypeArguments()[0];
}
#Override
public List<T> deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JsonProcessingException {
final ObjectCodec objectCodec = p.getCodec();
final JsonNode listOrObjectNode = objectCodec.readTree(p);
final List<T> result = new ArrayList<T>();
if (listOrObjectNode.isArray()) {
for (JsonNode node : listOrObjectNode) {
result.add(objectCodec.treeToValue(node, cls));
}
} else {
result.add(objectCodec.treeToValue(listOrObjectNode, cls));
}
return result;
}
}
...
public class ListOrObjectResultItemJsonDeserializer extends ListOrObjectGenericJsonDeserializer<ResultItem> {}
Next you need to annotate your POJO field. Let's say you have classes like Result and ResultItem:
public class Result {
// here you add your custom deserializer so jackson will be able to use it
#JsonDeserialize(using = ListOrObjectResultItemJsonDeserializer.class)
private List<ResultItem> result;
public void setResult(final List<ResultItem> result) {
this.result = result;
}
public List<ResultItem> getResult() {
return result;
}
}
...
public class ResultItem {
private String value;
public String getValue() {
return value;
}
public void setValue(final String value) {
this.value = value;
}
}
Now you can check your deserializer:
// list of values
final String json1 = "{\"result\": [{\"value\": \"test\"}]}";
final Result result1 = new ObjectMapper().readValue(json1, Result.class);
// one value
final String json2 = "{\"result\": {\"value\": \"test\"}}";
final Result result2 = new ObjectMapper().readValue(json2, Result.class);
result1 and result2 contain the same value.
You can achieve what you want with a configuration flag in Jackson's ObjectMapper:
ObjectMapper mapper = Jackson2ObjectMapperBuilder.json()
.featuresToEnable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
.build();
Just set this ObjectMapper instance to your RestTemplate as explained in this answer, and in the class you are deserializing to, always use a collection, i.e. a List:
public class Response {
private List<Result> result;
// getter and setter
}
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.
This is related to a previous question that I asked here earlier
JSON parsing using Gson
I am trying to parse the same JSON, but now I have changed my classes a little bit.
{
"lower": 20,
"upper": 40,
"delimiter": " ",
"scope": ["${title}"]
}
My class now looks like:
public class TruncateElement {
private int lower;
private int upper;
private String delimiter;
private List<AttributeScope> scope;
// getters and setters
}
public enum AttributeScope {
TITLE("${title}"),
DESCRIPTION("${description}"),
private String scope;
AttributeScope(String scope) {
this.scope = scope;
}
public String getScope() {
return this.scope;
}
}
This code throws an exception,
com.google.gson.JsonParseException: The JsonDeserializer EnumTypeAdapter failed to deserialized json object "${title}" given the type class com.amazon.seo.attribute.template.parse.data.AttributeScope
at
The exception is understandable, because as per the solution to my previous question, GSON is expecting the Enum objects to be actually be created as
${title}("${title}"),
${description}("${description}");
But since this is syntactically impossible, what are the recommended solutions, workarounds?
I want to expand a bit NAZIK/user2724653 answer (for my case). Here is a Java code:
public class Item {
#SerializedName("status")
private Status currentState = null;
// other fields, getters, setters, constructor and other code...
public enum Status {
#SerializedName("0")
BUY,
#SerializedName("1")
DOWNLOAD,
#SerializedName("2")
DOWNLOADING,
#SerializedName("3")
OPEN
}
}
in the json file you have just a field "status": "N",, where N=0,1,2,3 - depend on the Status values. So that's all, GSON works fine with the values for the nested enum class. In my case i've parsed a list of Items from json array:
List<Item> items = new Gson().<List<Item>>fromJson(json,
new TypeToken<List<Item>>(){}.getType());
From the documentation for Gson:
Gson provides default serialization and deserialization for Enums... If you would prefer to change the default representation, you can do so by registering a type adapter through GsonBuilder.registerTypeAdapter(Type, Object).
Following is one such approach.
import java.io.FileReader;
import java.lang.reflect.Type;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
public class GsonFoo
{
public static void main(String[] args) throws Exception
{
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(AttributeScope.class, new AttributeScopeDeserializer());
Gson gson = gsonBuilder.create();
TruncateElement element = gson.fromJson(new FileReader("input.json"), TruncateElement.class);
System.out.println(element.lower);
System.out.println(element.upper);
System.out.println(element.delimiter);
System.out.println(element.scope.get(0));
}
}
class AttributeScopeDeserializer implements JsonDeserializer<AttributeScope>
{
#Override
public AttributeScope deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException
{
AttributeScope[] scopes = AttributeScope.values();
for (AttributeScope scope : scopes)
{
if (scope.scope.equals(json.getAsString()))
return scope;
}
return null;
}
}
class TruncateElement
{
int lower;
int upper;
String delimiter;
List<AttributeScope> scope;
}
enum AttributeScope
{
TITLE("${title}"), DESCRIPTION("${description}");
String scope;
AttributeScope(String scope)
{
this.scope = scope;
}
}
Use annotation #SerializedName:
#SerializedName("${title}")
TITLE,
#SerializedName("${description}")
DESCRIPTION
The following snippet removes the need for explicit Gson.registerTypeAdapter(...), using the #JsonAdapter(class) annotation, available since Gson 2.3 (see comment pm_labs).
#JsonAdapter(Level.Serializer.class)
public enum Level {
WTF(0),
ERROR(1),
WARNING(2),
INFO(3),
DEBUG(4),
VERBOSE(5);
int levelCode;
Level(int levelCode) {
this.levelCode = levelCode;
}
static Level getLevelByCode(int levelCode) {
for (Level level : values())
if (level.levelCode == levelCode) return level;
return INFO;
}
static class Serializer implements JsonSerializer<Level>, JsonDeserializer<Level> {
#Override
public JsonElement serialize(Level src, Type typeOfSrc, JsonSerializationContext context) {
return context.serialize(src.levelCode);
}
#Override
public Level deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) {
try {
return getLevelByCode(json.getAsNumber().intValue());
} catch (JsonParseException e) {
return INFO;
}
}
}
}
With GSON version 2.2.2 enum will be marshalled and unmarshalled easily.
import com.google.gson.annotations.SerializedName;
enum AttributeScope
{
#SerializedName("${title}")
TITLE("${title}"),
#SerializedName("${description}")
DESCRIPTION("${description}");
private String scope;
AttributeScope(String scope)
{
this.scope = scope;
}
public String getScope() {
return scope;
}
}
If you really want to use the Enum's ordinal value you can register a type adapter factory to override Gson's default factory.
public class EnumTypeAdapter <T extends Enum<T>> extends TypeAdapter<T> {
private final Map<Integer, T> nameToConstant = new HashMap<>();
private final Map<T, Integer> constantToName = new HashMap<>();
public EnumTypeAdapter(Class<T> classOfT) {
for (T constant : classOfT.getEnumConstants()) {
Integer name = constant.ordinal();
nameToConstant.put(name, constant);
constantToName.put(constant, name);
}
}
#Override public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
return nameToConstant.get(in.nextInt());
}
#Override public void write(JsonWriter out, T value) throws IOException {
out.value(value == null ? null : constantToName.get(value));
}
public static final TypeAdapterFactory ENUM_FACTORY = new TypeAdapterFactory() {
#SuppressWarnings({"rawtypes", "unchecked"})
#Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {
Class<? super T> rawType = typeToken.getRawType();
if (!Enum.class.isAssignableFrom(rawType) || rawType == Enum.class) {
return null;
}
if (!rawType.isEnum()) {
rawType = rawType.getSuperclass(); // handle anonymous subclasses
}
return (TypeAdapter<T>) new EnumTypeAdapter(rawType);
}
};
}
Then just register the factory.
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(EnumTypeAdapter.ENUM_FACTORY)
.create();
use this method
GsonBuilder.enableComplexMapKeySerialization();