JSON Consumer of Polymorphic Objects

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:

  1. 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.
  2. @JsonTypeInfo and @JsonSubTypes are meant for inheritance e.g class FooEventPayload extends EventPayload. In your case EventPayload<T> is a generic class and Jackson needs to be told what T is with a TypeReference 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



Leave a reply



Submit