Reputation: 9072
Consider the following example:
package com.example;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
public class JacksonDeserializationOfNamedTypes {
public static void main(String[] args) throws Exception {
ObjectMapper jackson = new ObjectMapper();
jackson.enableDefaultTypingAsProperty(DefaultTyping.JAVA_LANG_OBJECT, "@type");
Balloon redBalloon = new Balloon("red");
String json = jackson.writeValueAsString(redBalloon); //{"@type":"Balloon","color":"red"}
//assume the JSON could be anything
Object deserialized = jackson.readValue(json, Object.class);
assert deserialized instanceof Balloon;
assert redBalloon.equals(deserialized);
}
@JsonTypeName("Balloon")
@JsonTypeInfo(use = Id.NAME)
public static final class Balloon {
private final String color;
//for deserialization
private Balloon() {
this.color = null;
}
public Balloon(final String color) {
this.color = color;
}
public String getColor() {
return color;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
final Balloon other = (Balloon) obj;
return this.color.equals(other.color);
}
@Override
public int hashCode() {
int result = color.hashCode();
result = 31 * result + color.hashCode();
return result;
}
@Override
public String toString() {
return color + " balloon";
}
}
}
The deserialization fails at runtime with the following exception:
Exception in thread "main" java.lang.IllegalArgumentException: Invalid type id 'Balloon' (for id type 'Id.class'): no such class found
The produced JSON certainly has all the information Jackson needs to determine the type correctly, so how can I configure the ObjectMapper to properly map "Balloon"
to com.example.JacksonDeserializationOfNamedTypes$Balloon
?
Upvotes: 5
Views: 21430
Reputation: 108
SWince I just stumbled across the same issue, I want to add to the solution aboce to provide the fully qualified name, that you don't need to type it everytime, but you can do it like this:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
@JsonTypeInfo(use = Id.CLASS, include = As.WRAPPER_OBJECT, property = "@class")
public interface JsonStorable {}
Then you can setup your mapper
ObjectMapper objectMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
and that was all for me to do. It even handles polymorphic classes. Instead of the WRAPPER_OBJECT you can also use the property mentioned above. The result is like this
{
"de.firegate.jsonDb.User" : {
"username" : "user",
"password" : "pass",
"createdAt" : "1970-01-01T12:00:00",
"usergroups" : [ "Editor", "Author" ],
"pets" : [ {
"de.firegate.jsonDb.Cat" : {
"name" : "Maunz",
"favoriteToy" : "Bell"
}
}, {
"de.firegate.jsonDb.Dog" : {
"name" : "Wuff",
"breed" : "Sheppard",
"medals" : 7
}
}, {
"de.firegate.jsonDb.special.Bird" : {
"name" : "Butschi"
}
} ]
}
}
Upvotes: 0
Reputation: 21
One more way to achieve dynamic type resolution is using annotation @JsonTypeIdResolver
and custom implementation of interface TypeIdResolver
(there is base abstract implementation com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase
).
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "jsonPropertyWithSomeIdToReferClass")
@JsonTypeIdResolver(YourTypeIdResolver.class)
class YourBaseClass{}
Examples:
Also reflection library can help to find classes in class path:
Reflections reflections = new Reflections("my.project");
Set<Class<? extends SomeType>> subTypes = reflections.getSubTypesOf(SomeType.class);
Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(SomeAnnotation.class);
To avoid adding vulnerabilities to your code related to polymorphic Jackson features can help articles like this.
Upvotes: 1
Reputation: 9072
My current solution involves a combination of a custom deserializer and a manually formed map of type names to Java types:
package com.example.jackson;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class JacksonDeserializerOfNamedTypes extends StdDeserializer<Object> {
private final Map<String, Class<?>> typesByName;
private final String typeProperty;
private JacksonDeserializerOfNamedTypes(final Map<String, Class<?>> typesByName, final String typeProperty) {
super(Object.class);
this.typesByName = typesByName;
this.typeProperty = typeProperty;
}
@Override
public Object deserialize(final JsonParser parser, final DeserializationContext context) throws IOException, JsonProcessingException {
final ObjectCodec codec = parser.getCodec();
final JsonNode root = parser.readValueAsTree();
final JsonNode typeNameNodeOrNull = root.get(typeProperty);
if (typeNameNodeOrNull == null) {
throw new JsonMappingException(parser, "Unable to determine Java type of JSON: " + root);
} else {
final String typeName = typeNameNodeOrNull.asText();
return Optional
.ofNullable(typesByName.get(typeName))
.map(type -> parseOrNull(root, type, codec))
.orElseThrow(() ->
new JsonMappingException(parser, String.format(
"Unsupported type name '%s' in JSON: %s", typeName, root)));
}
}
private <T> T parseOrNull(final JsonNode root, final Class<T> type, final ObjectCodec codec) {
try {
return root.traverse(codec).readValueAs(type);
} catch (IOException e) {
return null;
}
}
public static void main(String[] args) throws Exception {
final Map<String, Class<?>> typesByName = scanForNamedTypes();
final SimpleModule namedTypesModule = new SimpleModule("my-named-types-module");
namedTypesModule.addDeserializer(Object.class, new JacksonDeserializerOfNamedTypes(typesByName, JsonTypeInfo.Id.NAME.getDefaultPropertyName()));
final Car pinto = new Car("Ford", "Pinto", 1971);
final Balloon sharik = new Balloon("blue");
final ObjectMapper mapper = new ObjectMapper().registerModule(namedTypesModule);
System.out.println(mapper.readValue(mapper.writeValueAsString(pinto), Object.class).getClass());
System.out.println(mapper.readValue(mapper.writeValueAsString(sharik), Object.class).getClass());
}
@JsonTypeName("Balloon")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
public static final class Balloon {
public String color;
private Balloon() {}
public Balloon(final String color) {
this.color = color;
}
}
@JsonTypeName("Car")
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
public static final class Car {
public String make;
public String model;
public int year;
private Car() {}
public Car(final String make, final String model, final int year) {
this.make = make;
this.model = model;
this.year = year;
}
}
static Map<String, Class<?>> scanForNamedTypes() {
//in reality, i'd be using a framework (e.g. Reflections) to scan the classpath
//for classes tagged with @JsonTypeName to avoid maintaining manual mappings
final Map<String, Class<?>> typesByName = new HashMap<>();
typesByName.put("Balloon", Balloon.class);
typesByName.put("Car", Car.class);
return Collections.unmodifiableMap(typesByName);
}
}
Upvotes: 3
Reputation: 279880
The produced JSON certainly has all the information Jackson needs to determine the type correctly
You've provided Jackson with the following
Object deserialized = jackson.readValue(json, Object.class);
Object
is the supertype of all Java reference types. This is known. Not very useful to Jackson. Your JSON also contains
{"@type":"Balloon","color":"red"}
Given that and thanks to
jackson.enableDefaultTypingAsProperty(DefaultTyping.JAVA_LANG_OBJECT, "@type");
Jackson can deduce that it can use the value for the @type
element. However, what can it do with the name Balloon
? Jackson doesn't know about all types on the classpath. Do you have a type with the fully qualified name Balloon
? Do you have a type named com.example.Balloon
or one named org.company.toys.Balloon
? How should Jackson choose?
The @JsonTypeInfo
and family of annotations are typically used for deserialization of inheritance hierarchies. For example
@JsonTypeInfo(use = Id.NAME)
@JsonSubTypes(value = @Type(value = Balloon.class))
public abstract static class Toy {
}
@JsonTypeName("Balloon")
public static final class Balloon extends Toy {
and
Object deserialized = jackson.readValue(json, Toy.class);
Now Jackson can look up the Toy
class and its metadata which identifies its subclass(es) as Balloon
which it can also check for the name Balloon
.
If you're not trying to model an inheritance hierarchy, the simplest solution, here, would be to use the fully qualified name of your Balloon
class in the @JsonTypeName
annotation.
@JsonTypeName("com.example.JacksonDeserializationOfNamedTypes$Balloon")
@JsonTypeInfo(use = Id.NAME)
public static final class Balloon {
That name will then appear in the JSON and Jackson will use it to determine the target class.
Upvotes: 4
Reputation: 13702
In short (and too late :) )
You should either:
provide the fully qualified className so Jackson can find the class to deserialize to, like:
@JsonTypeName("com.example.JacksonDeserializationOfNamedTypes$Balloon")
or add a custom deserializer to handle your Balloon
Type
Upvotes: -1