Polymorphic deserialization of JSON file with Jackson - java

Depending on the content of a JSON file, I want to deserialize it either to a superclass or subclass.
It should be deserialized to the superclass if it looks like this:
{
"id":"123",
"title":"my title",
"body":"my body"
}
Or to the subclass if it looks like this:
{
"id":"123",
"title":"my title",
"body":"my body",
"tags":["tag1", "tag2"]
}
So the only difference is the tags array, which should be deserialized to a String array.
But if I trigger the deserialization in Jersey (Dropwizard) via POST request, it returns {"code":400,"message":"Unable to process JSON"}.
This is the superclass:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
#JsonSubTypes({ #JsonSubTypes.Type(name = "subdocument", value = SubDocument.class) })
public class SuperDocument {
private String id;
private String title;
private String body;
public SuperDocument() {
}
#JsonCreator
public SuperDocument(#JsonProperty("id") String id, #JsonProperty("title") String title, #JsonProperty("body") String body) {
this.id = id;
this.title = title;
this.body = body;
}
#JsonProperty("id")
public String getId() {
return id;
}
#JsonProperty("id")
public void setId(String id) {
this.id = id;
}
... the other getters and setters ...
}
This is the subclass:
#JsonTypeName("subdocument")
public class SubDocument extends SuperDocument {
private String[] tags;
public SubDocument() {
}
#JsonCreator
public SubDocument(#JsonProperty("id") String id, #JsonProperty("title") String title, #JsonProperty("body") String body, #JsonProperty("tags") String[] tags) {
super(id, title, body);
this.tags = tags;
}
#JsonProperty("tags")
public String[] getTags() {
return tags;
}
#JsonProperty("tags")
public void setTags(String[] tags) {
this.tags = tags;
}
}
Do you know what I am doing wrong?

JsonTypeInfo require a property that can identify your sub-class/super class. For eg:
{
"id":"123",
"title":"my title",
"body":"my body",
"type":"superdocument"
}
and
{
"id":"123",
"title":"my title",
"body":"my body",
"tags":["tag1", "tag2"],
"type":"subdocument"
}
Then modify SuperDocument annotations as shown below.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,property="type")
#JsonSubTypes({ #JsonSubTypes.Type(name = "subdocument", value = SubDocument.class),#JsonSubTypes.Type(name = "superdocument", value = SuperDocument.class) })
public class SuperDocument {
}
If you don't want to intrduce an additional property "type", then you may have to write a custom type resolver and type deserializer as shown below.
public class DocumentTypeResolver extends StdTypeResolverBuilder {
#Override
public TypeDeserializer buildTypeDeserializer(
final DeserializationConfig config, final JavaType baseType, final Collection<NamedType> subtypes) {
return new DocumentDeserializer(baseType, null,
_typeProperty, _typeIdVisible, _defaultImpl);
}
}
Custom TypeDeserializer
public static class DocumentDeserializer extends AsPropertyTypeDeserializer {
public DocumentDeserializer(final JavaType bt, final TypeIdResolver idRes, final String typePropertyName, final boolean typeIdVisible, final Class<?> defaultImpl) {
super(bt, idRes, typePropertyName, typeIdVisible, defaultImpl);
}
public DocumentDeserializer(final AsPropertyTypeDeserializer src, final BeanProperty property) {
super(src, property);
}
#Override
public TypeDeserializer forProperty(final BeanProperty prop) {
return (prop == _property) ? this : new DocumentDeserializer(this, prop);
}
#Override
public Object deserializeTypedFromObject(final JsonParser jp, final DeserializationContext ctxt) throws IOException {
JsonNode node = jp.readValueAsTree();
Class<?> subType =null;
JsonNode tags = node.get("tags");
if (tags == null) {
subType=SuperDocument.class;
} else {
subType=SubDocument.class;
}
JavaType type = SimpleType.construct(subType);
JsonParser jsonParser = new TreeTraversingParser(node, jp.getCodec());
if (jsonParser.getCurrentToken() == null) {
jsonParser.nextToken();
}
JsonDeserializer<Object> deser = ctxt.findContextualValueDeserializer(type, _property);
return deser.deserialize(jsonParser, ctxt);
}
}
Now annotate your SuperDocument class as shown below
#JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
#JsonTypeResolver(DocumentTypeResolver.class)
public class SuperDocument {
}

Related

Cannot deserialize generic class hierarchy using Jackson

I'm receiving, for instance, this JSON from an external vendor (where payload can be variable):
{
"payload": {
"enrolledAt": "2018-11-05T00:00:00-05:00",
"userId": "99c7ff5c-2c4e-423f-abeb-2e5f3709a42a"
},
"requestId": "80517bb8-2a95-4f15-9a73-fcf3752a1147",
"eventType": "event.success",
"createdAt": "2018-11-05T16:55:13.762-05:00"
}
I'm trying to model these using this class:
public final class Notification<T extends AbstractModel> {
#JsonProperty("requestId")
private String requestId;
#JsonProperty("eventType")
private String eventType;
#JsonProperty("createdAt")
private ZonedDateTime createdAt;
private T payload;
#JsonCreator
public Notification(#JsonProperty("payload") T payload) {
requestId = UUID.randomUUID().toString();
eventType = payload.getType();
createdAt = ZonedDateTime.now();
this.payload = payload;
}
// getters
}
...and then having these possible (generic) types:
public abstract class AbstractModel {
private String userId;
private Type type;
#JsonCreator
AbstractModel(#JsonProperty("companyUserId") String userId, #JsonProperty("type") Type type) {
this.userId = userId;
this.type = type;
}
// getters
public enum Type {
CANCEL("event.cancel"),
SUCCESS("event.success");
private final String value;
Type(String value) {
this.value = value;
}
public String getValue() { return value; }
}
}
public final class Success extends AbstractModel {
private ZonedDateTime enrolledAt;
#JsonCreator
public Success(String userId, #JsonProperty("enrolledAt") ZonedDateTime enrolledAt) {
super(userId, Type.SUCCESS);
this.enrolledAt = enrolledAt;
}
// getters
}
public final class Cancel extends AbstractModel {
private ZonedDateTime cancelledAt;
private String reason;
#JsonCreator
public Cancel(String userId, #JsonProperty("cancelledAt") ZonedDateTime cancelledAt,
#JsonProperty("reason") String reason) {
super(userId, Type.CANCEL);
this.cancelledAt = cancelledAt;
this.reason = reason;
}
// getters
}
The application is based on Spring Boot, so I'm deserializing the JSON like:
#Component
public final class NotificationMapper {
private ObjectMapper mapper;
public NotificationMapper(final ObjectMapper mapper) {
this.mapper = mapper;
}
public Optional<Notification<? extends AbstractModel>> deserializeFrom(final String thiz) {
try {
return Optional.of(mapper.readValue(thiz, new NotificationTypeReference()));
} catch (final Exception e) { /* log errors here */ }
return Optional.empty();
}
private static final class NotificationTypeReference extends TypeReference<Notification<? extends AbstractModel>> { }
}
...but eventually since I'm posting this right here, Jackson doesn't like any of that so far. I've tried several things like: JsonTypeInfo and JsonSubTypes, but I can't change the JSON input.
Anyone? Any clue(s)?
We ended up adding another key/value pair in the JSON – the contract was indeed modified a little bit to accommodate that "private" key/value pair.
In any case, if somebody runs into the same issue, this is the solution for the approach:
...
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
#JsonIgnoreProperties("_type")
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "_type")
#JsonSubTypes({
#JsonSubTypes.Type(value = Cancel.class, name = "_type"),
#JsonSubTypes.Type(value = Fail.class, name = "_type"),
#JsonSubTypes.Type(value = Success.class, name = "_type"),
})
public abstract class AbstractModel {
private String userId;
private Type type;
AbstractModel() { }
AbstractModel(final String userId, final Type type) {
this.userId = userId;
this.type = type;
}
// getters, toString, etc.
public enum Type {
CANCEL("event.cancelled"),
FAIL("event.failed"),
SUCCESS("event.success");
private final String value;
Type(String value) {
this.value = value;
}
public String getValue() { return value; }
}
}

Jackson JsonUnwrapped and generic

I have two classes:
public class ResponseInfo {
private final int code;
private final String description;
#JsonCreator
public static ResponseInfo of(
#JsonProperty("code") int code,
#JsonProperty("description") String description
) {
return new ResponseInfo(code, description);
}
private ResponseInfo(
int code,
String description
) {
this.code = code;
this.description = description;
}
#JsonProperty("code")
public int code() {
return code;
}
#JsonProperty("description")
public String description() {
return description;
}
}
and:
public class Response<T> {
private final ResponseInfo responseInfo;
private final T payload;
public static <T> Response<T> of(ResponseInfo responseInfo, T payload) {
return new Response<>(responseInfo, payload);
}
private Response(ResponseInfo responseInfo, T payload) {
this.responseInfo = responseInfo;
this.payload = payload;
}
#JsonUnwrapped
public ResponseInfo responseInfo() {
return responseInfo;
}
#JsonUnwrapped
public T payload() {
return payload;
}
}
I use them to add additional info into response (as code and description). For example:
Response.of(ResponseInfo.of(0, "OK"), User.of("Oleg", 23))
will be serialized into:
{
"age": 23,
"code": 0,
"description": "OK",
"name": "Oleg"
}
How the deserialization of Response can be done?
I can't use #JsonProperty in #JsonCreator directly cause I don't know properties of payload.
JsonCreator with #JsonUnwrapped also doesn't work.
I am using jackson-datatype-jdk8:2.9.5.
I've created implementation.
public class ResponseDeserializer
extends JsonDeserializer<Response<?>>
implements ContextualDeserializer {
private JavaType type;
#SuppressWarnings("unused")
public ResponseDeserializer() {
}
private ResponseDeserializer(JavaType type) {
this.type = type;
}
#Override
public JsonDeserializer<?> createContextual(
DeserializationContext context,
BeanProperty beanProperty
) {
JavaType contextualType = context.getContextualType();
if(contextualType == null) {
contextualType = beanProperty.getMember()
.getType();
}
if (!contextualType.isTypeOrSubTypeOf(Response.class)) {
throw new IllegalArgumentException("contextualType should be " + Response.class.getName());
}
final JavaType payloadType = contextualType.containedType(0);
return new ResponseDeserializer(payloadType);
}
#Override
public Response<?> deserialize(
JsonParser jsonParser,
DeserializationContext context
) throws IOException {
final ObjectCodec codec = jsonParser.getCodec();
JsonNode rootNode = codec.readTree(jsonParser);
final ResponseInfo responseInfo = ResponseInfo.of(
rootNode.get("code").asInt(),
rootNode.get("description").asText()
);
final JsonNode payloadNode = createPayloadNode(rootNode, codec);
final JsonParser payloadParser = payloadNode.traverse();
final Object payload = codec.readValue(payloadParser, type);
return Response.of(responseInfo, payload);
}
private JsonNode createPayloadNode(JsonNode rootNode, ObjectCodec codec) {
final Map<String, JsonNode> remainingNodes = findRemainingNodes(rootNode);
if(remainingNodes.size() == 1) {
final JsonNode payloadNode = remainingNodes.get("payload");
if(payloadNode != null && !payloadNode.isObject()) {
return payloadNode;
}
}
return buildRemainingNode(remainingNodes, codec);
}
private JsonNode buildRemainingNode(Map<String, JsonNode> remainingNodes, ObjectCodec codec) {
final ObjectNode remainingNode = (ObjectNode) codec.createObjectNode();
remainingNodes.forEach(remainingNode::set);
return remainingNode;
}
private Map<String, JsonNode> findRemainingNodes(JsonNode rootNode) {
Map<String, JsonNode> remainingNodes = new HashMap<>();
rootNode.fields()
.forEachRemaining(entry -> {
final String key = entry.getKey();
if(key.equals("code") || key.equals("description")) {
return;
}
remainingNodes.put(key, entry.getValue());
});
return remainingNodes;
}
}

Deserializing generic java object returns LinkedTreeMap

I have a generic Java Message object that's represented by the following json string:
{
"type": "Example",
"processTime": 3.4,
"payload":
{
"id": "someString",
"type": "anotherString",
"message": "yetAnotherString"
}
}
The Java Message object is generic. I also have an object called Event. When trying to convert the json into a Message<Event> object using gson, a Message object is returned with the correct json values, but the nested generic object is somehow returned as a "LinkedTreeMap" object instead of an Event object. I know this has something to do with type erasure, but I still can't seem to figure out how to return a Message<Event> from the json.
This is my main():
public class App {
public static void main(String[] args) {
//The json string to convert into a "Message<Event>" object
String jsonString = "{\"type\":\"Example\",\"processTime\":3.4,\"payload\":{\"id\":\"someString\",\"type\":\"anotherString\",\"message\":\"yetAnotherString\"}}";
Message<Event> message = new Message<Event>();
message = message.convertJsonToObject(jsonString, Event.class);
System.out.println(message.getClass().getName()); //returns as a "Message" class -- as expected
System.out.println(message.getPayload().getClass().getName()); //returns as a "LinkedTreeMap" instead of an "Event" object
}
}
Message class:
public class Message<T> {
private String type;
private double processTime;
private T payload;
public Message(String type, double processTime, T payload) {
this.type = type;
this.processTime = processTime;
this.payload = payload;
}
public Message() {
type = null;
processTime = 0;
payload = null;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public double getProcessTime() {
return processTime;
}
public void setProcessTime(double processTime) {
this.processTime = processTime;
}
public T getPayload() {
return payload;
}
public void setPayload(T payload) {
this.payload = payload;
}
public Message<T> convertJsonToObject(String jsonString, Class<T> classType) {
GsonBuilder gson = new GsonBuilder();
Type collectionType = new TypeToken<Message<T>>() {}.getType();
Message<T> myMessage = gson.create().fromJson(jsonString, collectionType);
return myMessage;
}
#Override
public String toString() {
return new Gson().toJson(this);
}
}
Event class:
public class Event {
private String id;
private String type;
private String message;
public Event(String id, String type, String message) {
this.id = id;
this.type = type;
this.message = message;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
#Override
public String toString() {
return new Gson().toJson(this);
}
}

Unable to deserialiaze Jaxrs response entity get

im using below code
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
WebTarget webTarget = client.target(uri);
Builder builder = webTarget.request();
Response response = builder.accept(MediaType.APPLICATION_JSON).get(Response.class);
final List<MyResponse> accountList = response.readEntity(new GenericType<List<MyResponse>>(){});
This returns accountList but all the values inside the list Objects were **null ie(Each property value inside MyResponse object is null)
But If i use below code
String myResponse = response
.readEntity(String.class);
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
MyResponse[] obj = mapper.readValue(myResponse, MyResponse[].class);
obj returns the proper array of Objects..but i dont want to read as string and deserialize..Please suggest!
Uri response is as follows
[
{
"type": "A1",
"attrs": {
"test_card": "Y"
}
}, {
"type": "A2"
"attrs": {
"issue_card": "N"
}
}
]
MyResponse Object
#XmlRootElement
#XmlAccessorType(XmlAccessType.FIELD)
#JsonIgnoreProperties(ignoreUnknown = true)
public class MyResponse implements Serializable {
#JsonProperty("type")
private String Type;
#JsonProperty("attrs")
private MyAttributes myAttributes;
public MyAttributes getattrs() {
return myAttributes;
}
public void setattrs(MyAttributes myAttributes) {
this.myAttributes = myAttributes;
}
public MyResponse() {
}
public String gettype() {
return Type;
}
public void settype(String Type) {
this.Type = Type;
}
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
}
MyAttributes Object
#JsonIgnoreProperties(ignoreUnknown = true)
public class MyAttributes implements Serializable {
/**
*
*/
private static final long serialVersionUID = -4************;
#JsonProperty("test_card")
private String testCard;
public DecisionActionAttributes() {
}
public String getNewCardInd() {
return testCard;
}
public void setNewCardInd(String testCard) {
this.testCard = testCard;
}
#Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this, false);
}
#Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}

Convert json into an object of a class that contains a template variable using gson

My Api response looks like this.
{
"status" : 1,
"message" : "Some Message",
"data" : {
}
}
My Response class looks like this. The type of data changes depending on the request being made.
public class Response<T>{
#Expose
#SerializedName("status")
private Integer status;
#Expose
#SerializedName("message")
private String message;
#Expose
#SerializedName("data")
private T data;
//Getters and Setters
}
Question 1. How to use gson to parse this json?
Response<ClassA> response = new Gson().fromJson(jsonString, ??);
Question 2. How would i write Parcelable implementation for this class.
dest.writeInt(status == null ? 0:status);
dest.writeString(message);
??
You need to do some changes in your Model structure. To do this..
Create a BaseDTO which is nothing but your Response class and extend your BaseDTO with your ClassA.
BaseDto.class
public class BaseDto {
#Expose
#SerializedName("status")
protected Integer status;
#Expose
#SerializedName("message")
protected String message;
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
ClassA.class
public class ClassA extends BaseDto implements Parcelable {
String name;
protected ClassA(Parcel in) {
name = in.readString();
status = in.readByte() == 0x00 ? null : in.readInt();
message = in.readString();
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
if (status == null) {
dest.writeByte((byte) (0x00));
} else {
dest.writeByte((byte) (0x01));
dest.writeInt(status);
}
dest.writeString(message);
}
#SuppressWarnings("unused")
public static final Parcelable.Creator<ClassA> CREATOR = new Parcelable.Creator<ClassA>() {
#Override
public ClassA createFromParcel(Parcel in) {
return new ClassA(in);
}
#Override
public ClassA[] newArray(int size) {
return new ClassA[size];
}
};
}
Use this site to generate Parcelable class http://www.parcelabler.com/
to parse using gson you just need to pass it's type to it..
I have written a function to simplify this..
Save these in a Util class
public static <T> String convertObjectToStringJson(T someObject, Type type) {
Gson gson = new Gson();
String strJson = gson.toJson(someObject, type);
return strJson;
}
public static <T> T getObjectFromJson(String json, Type type) {
Gson gson = new Gson();
if (json != null) {
if (json.isEmpty()) {
return null;
}
}
return gson.fromJson(json, type);
}
example to use these function:
ClassA classA = Util.getObjectFromJson(strJson, new TypeToken<ClassA>() {}.getType());
String jsonClassA = Util.convertObjectToStringJson(objClassA, new TypeToken<ClassA>() {}.getType());
Answering my own question.
Question 1:
Response<ClassA> response = new Gson().fromJson(jsonString, new TypeToken<Response<ClassA>>(){}.getType());
Question 2:
Changed class to this.
public class Response<T extends Parcelable>{
#Expose
#SerializedName("status")
private Integer status;
#Expose
#SerializedName("message")
private String message;
#Expose
#SerializedName("data")
private T data;
//Getters and Setters
#Override
public void writeToParcel(Parcel dest, int flags) {
if (data != null) {
dest.writeString(data.getClass().getName());
dest.writeParcelable(data, flags);
} else dest.writeString(null);
dest.writeString(message);
dest.writeInt(status);
}
}
protected Response(Parcel in) {
String className = in.readString();
if (className != null) {
try {
data = in.readParcelable(Class.forName(className).getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
message = in.readString();
status = in.readInt();
}

Categories

Resources