So I have JSON that looks like this:
{
"ActivityDisplayModel" : {
"name" : "lunch with friends",
"startTime" : "12:00:00",
"type" : {
"id" : "MEAL",
"description" : "Meal"
},
"complete" : false
}
}
I'm trying to find the way to get @JsonTypeInfo
to not be mad at me for having the type parameter inside the type
object. I've got this working before when the field type
was a String and not an object itself, but for later processing I need it as an object. I know the following doesn't work, and I'm guessing theres a way to use JsonTypeInfo.Id.CUSTOM, but after looking all over on the internet, no full examples with JSON have come up. Also, if this is possible with an objectMapper setting, I'm all ears.
/**
* My ActivityDisplayModel Abstract Class
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type.id")
@JsonSubTypes({
@JsonSubTypes.Type(value = MealDisplayModel.class, name = "MEAL"),
@JsonSubTypes.Type(value = EntertainmentDisplayModel.class, name = "ENTERTAINMENT")
})
public abstract class ActivityDisplayModel {
...
The above is essentially what I want to do, but of course I get an exception of:
Could not read JSON: Could not resolve type id '{' into a subtype of [simple type, class ... .ActivityDisplayModel]
For such a simple problem of just looking one level deeper in the JSON, who would have thought it would have been so much trouble?
NAME. public static final JsonTypeInfo.Id NAME. Means that logical type name is used as type information; name will then need to be separately resolved to actual concrete type (Class).
@JsonTypeInfo is used to indicate details of type information which is to be included in serialization and de-serialization.
@JsonTypeName is used to set type names to be used for annotated class.
Annotation Type JsonSubTypesAnnotation used with JsonTypeInfo to indicate sub types of serializable polymorphic types, and to associate logical names used within JSON content (which is more portable than using physical Java class names).
I know it's been 3 years since the original question, but dot-nested properties are still not supported and maybe this will help someone out. I ended up creating a class NestedTypeResolver
so we can use the dot-syntax as expected. Simply add @JsonTypeResolver(NestedTypeResolver.class)
to any class with nested discriminators and the poster's original attempt will work:
/**
* My ActivityDisplayModel Abstract Class
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type.id")
@JsonSubTypes({
@JsonSubTypes.Type(value = MealDisplayModel.class, name = "MEAL"),
@JsonSubTypes.Type(value = EntertainmentDisplayModel.class, name = "ENTERTAINMENT")
})
@JsonTypeResolver(NestedTypeResolver.class)
public abstract class ActivityDisplayModel {
NestedTypeResolver:
/**
* Allows using nested "dot" dyntax for type discriminators. To use, annotate class with @JsonTypeResolver(NestedTypeResolver.class)
*/
public class NestedTypeResolver extends StdTypeResolverBuilder {
@Override
public TypeDeserializer buildTypeDeserializer(DeserializationConfig config, JavaType baseType,
Collection<NamedType> subtypes) {
//Copied this code from parent class, StdTypeResolverBuilder with same method name
TypeIdResolver idRes = idResolver(config, baseType, subtypes, false, true);
return new NestedTypeDeserializer(baseType, idRes, _typeProperty, _typeIdVisible,
null, _includeAs);
}
}
All the heavy work is done in here, NestedTypeDeserializer:
/**
* Heavy work to support {@link NestedTypeResolver}
*/
public class NestedTypeDeserializer extends AsPropertyTypeDeserializer {
private static final Logger LOGGER = LoggerFactory.getLogger(NestedTypeDeserializer.class);
public NestedTypeDeserializer(JavaType bt,
TypeIdResolver idRes, String typePropertyName, boolean typeIdVisible,
JavaType defaultImpl) {
super(bt, idRes, typePropertyName, typeIdVisible, defaultImpl);
}
public NestedTypeDeserializer(JavaType bt, TypeIdResolver idRes, String typePropertyName, boolean typeIdVisible,
JavaType defaultImpl, JsonTypeInfo.As inclusion) {
super(bt, idRes, typePropertyName, typeIdVisible, defaultImpl, inclusion);
}
public NestedTypeDeserializer(AsPropertyTypeDeserializer src, BeanProperty property) {
super(src, property);
}
@Override
public TypeDeserializer forProperty(BeanProperty prop) {
return (prop == _property) ? this : new NestedTypeDeserializer(this, prop);
}
@Override
public Object deserializeTypedFromObject(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode originalNode = p.readValueAsTree();
JsonNode node = originalNode;
//_typePropertyName is the dot separated value of "property" in @JsonTypeInfo
LOGGER.debug("Searching for type discriminator [{}]...", _typePropertyName);
for (String property : _typePropertyName.split("\\.")) { //traverse down any nested properties
JsonNode nestedProp = node.get(property);
if (nestedProp == null) {
ctxt.reportWrongTokenException(p, JsonToken.FIELD_NAME,
"missing property '" + _typePropertyName + "' that is to contain type id (for class "
+ baseTypeName() + ")");
return null;
}
node = nestedProp;
}
LOGGER.debug("Found [{}] with value [{}]", _typePropertyName, node.asText());
JsonDeserializer<Object> deser = _findDeserializer(ctxt, "" + node.asText());
//Since JsonParser is a forward-only operation and finding the "type" discriminator advanced the pointer, we need to reset it
//Got clues from https://www.dilipkumarg.com/dynamic-polymorphic-type-handling-jackson/
JsonParser jsonParser = new TreeTraversingParser(originalNode, p.getCodec());
if (jsonParser.getCurrentToken() == null) {
jsonParser.nextToken();
}
return deser.deserialize(jsonParser, ctxt);
}
}
Disclaimer: we've been using this for a month with Jackson 2.8.10 and have had no issues, but we had to go deep into the Jackson source code weeds to accomplish it, so YMMV. Hopefully Jackson will allow this out-of-the-box someday so we dont need these workarounds.
I am not sure that you can do it with specifying inner property: type.id
. In my opinion you should change your JSON to simpler version. If you can not force your JSON supplier to change JSON schema you have to do it manually. Assume that your JSON looks like below:
{
"activityDisplayModel": {
"name": "lunch with friends",
"type": {
"id": "MEAL",
"description": "Meal"
},
"complete": false
}
}
Below POJO classes fit to above JSON:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = MealDisplayModel.class, name = "MEAL"),
@JsonSubTypes.Type(value = EntertainmentDisplayModel.class, name = "ENTERTAINMENT")
})
abstract class ActivityDisplayModel {
protected String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
class MealDisplayModel extends ActivityDisplayModel {
private boolean complete;
public boolean isComplete() {
return complete;
}
public void setComplete(boolean complete) {
this.complete = complete;
}
@Override
public String toString() {
return "MealDisplayModel [complete=" + complete + ", toString()=" + super.toString() + "]";
}
}
@JsonIgnoreProperties("complete")
class EntertainmentDisplayModel extends ActivityDisplayModel {
@Override
public String toString() {
return "EntertainmentDisplayModel [toString()=" + super.toString() + "]";
}
}
class Root {
private ActivityDisplayModel activityDisplayModel;
public ActivityDisplayModel getActivityDisplayModel() {
return activityDisplayModel;
}
public void setActivityDisplayModel(ActivityDisplayModel activityDisplayModel) {
this.activityDisplayModel = activityDisplayModel;
}
@Override
public String toString() {
return activityDisplayModel.toString();
}
}
Below script shows how you can parse above JSON:
ObjectMapper mapper = new ObjectMapper();
// Updated JSON in memory
ObjectNode rootNode = (ObjectNode)mapper.readTree(json);
ObjectNode activityDisplayModelNode = (ObjectNode)rootNode.path("activityDisplayModel");
JsonNode typeNode = activityDisplayModelNode.path("type");
activityDisplayModelNode.set("type", typeNode.path("id"));
System.out.println("Result: " + mapper.convertValue(rootNode, Root.class));
Above script prints:
Result: MealDisplayModel [complete=false, toString()=lunch with friends]
Also see:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With