JSON consumer of polymorphic objects
Thanks to Eric's comment pointing me to programmerbruce I managed to crack it. Here's the code I used (cut down to simplify).
public static class Info {
@JsonProperty("Product")
public String product;
// Empty in the 0d version, One entry in the 1d version, two entries in the 2d version.
@JsonProperty("Dimensions")
public String[] dimensions;
}
public static class Info0d extends Info {
}
public static class Info1d extends Info {
@JsonProperty("Labels")
public String[] labels;
}
public static class Info2d extends Info {
@JsonProperty("Labels")
public String[][] labels;
}
public static class InfoDeserializer extends StdDeserializer<Info> {
public InfoDeserializer() {
super(Info.class);
}
@Override
public Info deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Class<? extends Info> variantInfoClass = null;
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
// Inspect the `diemnsions` field to decide what to expect.
JsonNode dimensions = root.get("Dimensions");
if ( dimensions == null ) {
variantInfoClass = Info0d.class;
} else {
switch ( dimensions.size() ) {
case 1:
variantInfoClass = Info1d.class;
break;
case 2:
variantInfoClass = Info2d.class;
break;
}
}
if (variantInfoClass == null) {
return null;
}
return mapper.readValue(root, variantInfoClass);
}
}
And to install this in the ObjectMapper
:
// Register the special deserializer.
InfoDeserializer deserializer = new InfoDeserializer();
SimpleModule module = new SimpleModule("PolymorphicInfoDeserializerModule", new Version(1, 0, 0, null));
module.addDeserializer(Info.class, deserializer);
mapper.registerModule(module);
factory = new JsonFactory(mapper);
Deserialize a polymorphic map with Jackson, where Type information is given within the string key
You are looking for an ObjectMapper
. Have a look at my answer to JSON POJO consumer of polymorphic objects for an example.
Jackson polymorphic deserialization with nested type info property
You are facing 2 issues here:
- As you have seen Jackson cannot easily and just by annotations use a property in a nested JSON object to deduce the type to deserialize to.
@JsonTypeInfo
and@JsonSubTypes
are meant for inheritance e.gclass FooEventPayload extends EventPayload
. In your caseEventPayload<T>
is a generic class and Jackson needs to be told whatT
is with aTypeReference
Look here for instance
Assuming you want a generic class I'd suggest serialize into a tree first, peek into the tree to get the property that specifies the type and then convert the tree to an object of this type. You can skip @JsonTypeInfo
and @JsonSubTypes
. E.g.
// user object mapper to parse JSON into a tree (node is the root)
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(jsonString);
// use get as many times as needed by depth
// to get the value that defines the type to deserialise to
String type = node.get("metadata").get("eventName").textValue();
// convert JsonNode variable to the required type
if (type.equals("fooEvent")) {
EventPayload<FooEvent> event =
mapper.convertValue(node, new TypeReference<EventPayload<FooEvent>>(){});
} else if (type.equals("barEvent")) {
EventPayload<BarEvent> event =
mapper.convertValue(node, new TypeReference<EventPayload<BarEvent>>(){});
}
Deserializing polymorphic types with Jackson based on the presence of a unique property
This feels like something @JsonTypeInfo
and @JsonSubTypes
should be used for but I've picked through the docs and none of the properties that can be supplied quite seem to match what you're describing.
You could write a custom deserializer that uses @JsonSubTypes
' "name" and "value" properties in a non-standard way to accomplish what you want. The deserializer and @JsonSubTypes
would be supplied on your base class and the deserializer would use the "name" values to check for the presence of a property and if it exists, then deserialize the JSON into the class supplied in the "value" property. Your classes would then look something like this:
@JsonDeserialize(using = PropertyPresentDeserializer.class)
@JsonSubTypes({
@Type(name = "stringA", value = SubClassA.class),
@Type(name = "stringB", value = SubClassB.class)
})
public abstract class Parent {
private Long id;
...
}
public class SubClassA extends Parent {
private String stringA;
private Integer intA;
...
}
public class SubClassB extends Parent {
private String stringB;
private Integer intB;
...
}
Gson: How do I parse polymorphic values that can be either lists or strings?
If you want to handle both scenarios you can use a custom deserializer. Of course, you have to change the "cust_id" variable to be a list or an array.
Main:
String json1 = "{\"cust_id\": \"87655\"}";
String json2 = "{\"cust_id\": [\"12345\", \"45678\"]}";
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Customer.class, new CustomerDeserializer());
Gson gson = gsonBuilder.create();
Customer customer1 = gson.fromJson(json1, Customer.class);
System.out.println(customer1);
Customer customer2 = gson.fromJson(json2, Customer.class);
System.out.println(customer2);
Customer
public class Customer {
@SerializedName("cust_id")
private List<String> custId;
public List<String> getCustId() {
return custId;
}
public void setCustId(List<String> custId) {
this.custId = custId;
}
}
CustomerDeserializer
public class CustomerDeserializer implements JsonDeserializer<Customer> {
@Override
public Customer deserialize(JsonElement jsonElement, Type typeOf, JsonDeserializationContext context) throws JsonParseException {
Customer result = null;
Gson gson = new Gson();
try {
// try to deserialize by assuming JSON has a list
result = gson.fromJson(jsonElement, Customer.class);
} catch (JsonSyntaxException jse) {
// error here means JSON has a single string instead of a list
try {
// get the single ID
String custId = jsonElement.getAsJsonObject().get("cust_id").getAsString();
result = new Customer();
result.setCustId(Arrays.asList(new String[] {custId}));
} catch (Exception e) {
// more error handling here
e.printStackTrace();
}
}
return result;
}
}
Output
Customer [custId=[87655]]
Customer [custId=[12345, 45678]]
Jackson deserialization of type with different objects
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.
Parsing an array of non-homogeneous JSON Objects with Jackson
I would use
com.fasterxml.jackson.databind.JsonNode
.
JsonNode parsed = objectMapper
.readValue("[{\"name\": \"a\"},{\"type\":\"b\"}]", JsonNode.class);
This class has tons of utility methods to work with.
Or specific for arrays you can use:
com.fasterxml.jackson.databind.node.ArrayNode
ArrayNode value = objectMapper
.readValue("[{\"name\": \"a\"},{\"type\":\"b\"}]", ArrayNode.class);
EDIT
Sorry, I have misread your question, you can use @JsonTypeInfo
for polymorphic serialization/deserialization:
public static void main(String args[]) throws JsonProcessingException {
//language=JSON
String data = "[{\"type\":\"type1\", \"type1Specific\":\"this is type1\"},{\"type\":\"type2\", \"type2Specific\":\"this is type2\"}]";
ObjectMapper objectMapper = new ObjectMapper();
List<BaseType> parsed = objectMapper.readValue(data, new TypeReference<List<BaseType>>() {});
System.out.println(parsed);
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", include = JsonTypeInfo.As.PROPERTY)
@JsonSubTypes(value = {
@JsonSubTypes.Type(value = Type1.class, name = "type1"),
@JsonSubTypes.Type(value = Type2.class, name = "type2")
})
static public abstract class BaseType {
public String type;
}
static public class Type1 extends BaseType {
public String type1Specific;
@Override
public String toString() {
return "Type1{" +
"type1Specific='" + type1Specific + '\'' +
'}';
}
}
static public class Type2 extends BaseType {
public String type2Specific;
@Override
public String toString() {
return "Type2{" +
"type2Specific='" + type2Specific + '\'' +
'}';
}
}
Here are the docs:
https://github.com/FasterXML/jackson-docs/wiki/JacksonPolymorphicDeserialization
Hope this helps.
And the result would be:
[Type1{type1Specific='this is type1'}, Type2{type2Specific='this is type2'}]
Related Topics
Writing Custom Kafka Serializer
Which Part of Throwing an Exception Is Expensive
Transactional Saves Without Calling Update Method
How to Check a Uploaded File Whether It Is an Image or Other File
Why Maven? What Are the Benefits
Is It Bad Practice to Make a Setter Return "This"
Find Files in a Folder Using Java
Difference Between Java Enumeration and Iterator
How to Use the Jersey JSON Pojo Support
Specific Difference Between Bufferedreader and Filereader
How to Convert Int to Unsigned Byte and Back
How to Cast an Object to an Int
How to Get the Http Status Code Out of a Servletresponse in a Servletfilter
Extract Source Code from .Jar File