I have class hierarchy A <- B <- C. I don't want class A to be aware of C, so I intend to use A.type="B" to indicate it to be class B, and then B.type2="C" to indicate it to be class C.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
#JsonSubTypes({#JsonSubTypes.Type(value = B.class, name = "B")})
public abstract class A {
private final String type;
public A(String type) {
this.type = type;
}
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type2")
#JsonSubTypes({#JsonSubTypes.Type(value = C.class, name = "C")})
public class B extends A {
private final String type2;
private final String propertyB;
#JsonCreator
public B(#JsonProperty("type2") String type2,
#JsonProperty("propertyB") String propertyB) {
super("B");
this.type2 = type2;
this.propertyB = propertyB;
}
}
public class C extends B {
private final String propertyC;
#JsonCreator
public C(#JsonProperty("propertyB") String propertyB,
#JsonProperty("propertyC") String propertyC) {
super("C", propertyB);
this.propertyC = propertyC;
}
}
When I read JSON of C to class A, the actual Java object is class B but not C.
#Test
void whenReadCJsonToA_thenObjectIsInstanceOfC() throws JsonProcessingException {
String json = "{\n" +
" \"type\" : \"B\",\n" +
" \"type2\" : \"C\",\n" +
" \"propertyB\" : \"b\",\n" +
" \"propertyC\" : \"c\"\n" +
"}";
A obj = objectMapper.readValue(json, A.class);
assertTrue(obj instanceof B, "obj is not instance of B"); // pass
assertTrue(obj instanceof C, "obj is not instance of C"); // fail
}
One way to make above test pass is writing custom deserializer, but this solution is tedious if the class holds many fields.
Is it possible to make above test pass with a more elegant way? Is my indention to cascade #JsonTypeInfo and #JsonSubTypes completely wrong?
My maven project can be found in Github.
According to this Jackson issue, multiple level inheritance is supported with only one type discliminator property. In my code, keep only property type and remove property type2.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
#JsonSubTypes({#JsonSubTypes.Type(value = B.class, name = "B")})
public abstract class A {
private final String type;
public A(String type) {
this.type = type;
}
}
#JsonSubTypes({#JsonSubTypes.Type(value = C.class, name = "C")})
public class B extends A {
private final String propertyB;
#JsonCreator
public B(#JsonProperty("propertyB") String propertyB) {
super("B");
this.propertyB = propertyB;
}
}
public class C extends B {
private final String propertyC;
#JsonCreator
public C(#JsonProperty("propertyB") String propertyB,
#JsonProperty("propertyC") String propertyC) {
super(propertyB);
this.propertyC = propertyC;
}
}
See full code in this commit.
This commit uses enum as type discliminator property type.
Related
I need to deserialize abstract objects, but I always get an exception. I have 3 abstract classes in which I need to connect with each other.
I have a classes structure that looks like the following:
public class Main {
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Child child = new Child(15, "child", "v1");
final String json = mapper.writeValueAsString(child);
final BaseClass baseClass = mapper.readValue(json, BaseClass.class);
System.out.println(baseClass);
}
}
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true,
property = "version"
)
#JsonSubTypes({
#JsonSubTypes.Type(value = People.class, name = "v1"),
#JsonSubTypes.Type(value = Animal.class, name = "v2"),
})
#JsonInclude(JsonInclude.Include.NON_NULL)
public abstract class BaseClass {
public String version;
public BaseClass(String version) {
this.version = version;
}
}
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true,
property = "type"
)
#JsonSubTypes({
#JsonSubTypes.Type(value = Dog.class, name = "dog")
})
public abstract class Animal extends BaseClass {
public String type;
public Animal(String type, String version) {
super(version);
}
}
public class Dog extends Animal {
public String name;
#JsonCreator
public Dog(#JsonProperty("name") String name, #JsonProperty("type") String type, #JsonProperty("version") String version) {
super(type, version);
this.name = name;
}
}
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
visible = true,
property = "type"
)
#JsonSubTypes({
#JsonSubTypes.Type(value = Child.class, name = "child")
})
public abstract class People extends BaseClass {
public String type;
public People(String type, String version) {
super(version);
this.type = type;
}
}
public class Child extends People {
public int age;
#JsonCreator
public Child(#JsonProperty("id") int age, #JsonProperty("type") String type, #JsonProperty("version") String version) {
super(type, version);
this.age = age;
}
}
When I tried to run this code, I got the following exception:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.test.People` (no Creators, like default constructor, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
at [Source: (String)"{"type":"child","version":"v1","age":15}"; line: 1, column: 27]
at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1764)
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1209)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserialize(AbstractDeserializer.java:274)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer._deserializeTypedForId(AsPropertyTypeDeserializer.java:135)
at com.fasterxml.jackson.databind.jsontype.impl.AsPropertyTypeDeserializer.deserializeTypedFromObject(AsPropertyTypeDeserializer.java:105)
at com.fasterxml.jackson.databind.deser.AbstractDeserializer.deserializeWithType(AbstractDeserializer.java:263)
at com.fasterxml.jackson.databind.deser.impl.TypeWrappedDeserializer.deserialize(TypeWrappedDeserializer.java:74)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3548)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3516)
at com.test.Main.main(Main.java:30)
What I do wrong? How can I fix this issue?
I have this class structure:
ResponseBody.java:
public class ResponseBody {
private String type;
private A a;
public static class A {
private List<B> b;
}
}
B.java:
public class B {
private String c;
private DynamicJson dynamicJson;
}
DynamicJson.java:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = Child1.class, name = "child-1"),
#JsonSubTypes.Type(value = Child2.class, name = "child-2"),
#JsonSubTypes.Type(value = Child3.class, name = "child-3")
})
public abstract class DynamicJson {
private String onlyKnownField;
}
Child1.java
#JsonTypeName("child-1")
public class Child1 extends DynamicJson {
// Number of fields and field names here change based on which implementation of Dynamic Json
}
Likewise, Child2 and Child3 classes.
Removed constructors, setters, getters.
I want to cast/deserialize below json to ResponseBody.java class.
{
"type": "child-1",
"a": {
"b": [
{
"c": "guid",
"dynamicJson": {
"onlyKnownField": "guid", // only field that always comes
/*
dynamic number of fields based on "type", say 2 fields, if type is
something else then number of fields here is different
*/
}
}
]
}
}
The field "dynamicJson" can have different number of fields with different names inside it based on "type" value. Which seems to be the problem,
This is the code being used:
ObjectMapper objectMapper = new ObjectMapper();
ResponseBody responseBody = objectMapper.readValue(json.getBody(), ResponseBody.class);
However, above code give this error:
Missing type id when trying to resolve subtype of [simple type, class DynamicJson]
What am I doing wrong here?
Let me know if other info is needed.
Is it possible to use multiple #JsonSubType annotations in a nested fashion?
For example, imagine the following classes:
#Data
#JsonSubTypeInfo(include=As.EXISTING_PROPERTY, property="species", use=Id.NAME, visible=true)
#JsonSubTypes({
#Type(name="Dog", value=Dog.class)
#Type(name="Cat", value=Cat.class)
})
public abstract class Animal {
private String name;
private String species;
}
#Data
#JsonSubTypeInfo(include=As.EXISTING_PROPERTY, property="breed", use=Id.NAME, visible=true)
#JsonSubTypes({
#Type(name="Labrador", value=Labrador.class)
#Type(name="Bulldog", value=Bulldog.class)
})
public abstract class Dog extends Animal {
private String breed;
}
#Data
public class Cat extends Animal {
private boolean lovesCatnip;
}
#Data
public class Labrador extends Dog {
private String color;
}
#Data
public class Bulldog extends Dog {
private String type; // "frenchy", "english", etc..
}
If I use an object mapper, I can successfully map a Bulldog to JSON, however, when trying to read the resulting JSON and read it back in, I get an error like the following:
Can not construct instance of com.example.Dog abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
Is it possible to get Jackson to work with such subtyping? Would I need to create a custom deserializer for each subclass?
EDIT:
I've modified the classes above slightly from the original posting. I added a Cat class and had that and Dog extend from Animal.
Here is a sample JSON that can be created using the ObjectMapper::writeValueAsString:
{
"name": null,
"species": "Dog",
"breed": "Bulldog",
"type": "B-Dog"
}
The following works if I use #JsonTypeInfo and a similar set up to yours. Maybe your problem is in your deserialization code, so have a look at this:
public class MyTest {
#Test
public void test() throws IOException {
final Bulldog bulldog = new Bulldog();
bulldog.setBreed("Bulldog");
bulldog.setType("B-Dog");
final ObjectMapper om = new ObjectMapper();
final String json = om.writeValueAsString(bulldog);
final Dog deserialized = om.readValue(json, Dog.class);
assertTrue(deserialized instanceof Bulldog);
}
#JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "species", use = Id.NAME, visible = true)
#JsonSubTypes({
#Type(name = "Dog", value = Dog.class),
#Type(name = "Cat", value = Cat.class)
})
public static abstract class Animal {
private String name;
private String species;
}
#JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "breed", use = Id.NAME, visible = true)
#JsonSubTypes({
#Type(name = "Labrador", value = Labrador.class),
#Type(name = "Bulldog", value = Bulldog.class)
})
public static abstract class Dog {
private String breed;
public String getBreed() {
return breed;
}
public void setBreed(final String breed) {
this.breed = breed;
}
}
public static abstract class Cat {
private String name;
}
public static class Labrador extends Dog {
private String color;
public String getColor() {
return color;
}
public void setColor(final String color) {
this.color = color;
}
}
public static class Bulldog extends Dog {
private String type; // "frenchy", "english", etc..
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
}
}
EDITed for the updated question: If you can use the same property (in the following code the hidden property "#class") for the inheritance hierarchy, it works:
#Test
public void test() throws IOException {
final Bulldog bulldog = new Bulldog();
// bulldog.setSpecies("Dog");
// bulldog.setBreed("Bulldog");
bulldog.setType("B-Dog");
final ObjectMapper om = new ObjectMapper();
final String json = om.writeValueAsString(bulldog);
final Animal deserialized = om.readValue(json, Animal.class);
assertTrue(deserialized instanceof Bulldog);
}
#JsonTypeInfo(include = As.PROPERTY, use = Id.CLASS, visible = false)
#JsonSubTypes({
#Type(Dog.class),
#Type(Cat.class)
})
public static abstract class Animal {
}
#JsonTypeInfo(include = As.PROPERTY, use = Id.CLASS, visible = false)
#JsonSubTypes({
#Type(name = "Labrador", value = Labrador.class),
#Type(name = "Bulldog", value = Bulldog.class)
})
public static abstract class Dog
extends Animal {
}
If you want to set the animal type (e.g. to compute species, breed etc.), you could also use this setup:
#Test
public void test() throws IOException {
final Bulldog bulldog = new Bulldog();
bulldog.setAnimalType("Bulldog");
// bulldog.setSpecies("Dog");
// bulldog.setBreed("Bulldog");
bulldog.setType("B-Dog");
final ObjectMapper om = new ObjectMapper();
final String json = om.writeValueAsString(bulldog);
System.out.println(json);
final Animal deserialized = om.readValue(json, Animal.class);
assertTrue(deserialized instanceof Bulldog);
}
#JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "animalType", use = Id.NAME, visible = true)
#JsonSubTypes({
#Type(Dog.class)
})
public static abstract class Animal {
private String animalType;
public String getAnimalType() {
return animalType;
}
public void setAnimalType(final String animalType) {
this.animalType = animalType;
}
}
#JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "animalType", use = Id.NAME, visible = true)
#JsonSubTypes({
#Type(value = Bulldog.class)
})
public static abstract class Dog
extends Animal {
}
#JsonTypeName("Bulldog")
public static class Bulldog extends Dog {
private String type; // "frenchy", "english", etc..
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
}
I was able to solve this such that the following JSON translates to a Bulldog object:
{
"species": "Dog",
"breed": "Bulldog",
"name": "Sparky",
"type": "English"
}
I used the following code to do this:
ObjectMapper om = new ObjectMapper();
om.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, JsonParser p, String msg) throws IOException {
JsonNode o = p.readValueAsTree();
JsonNode copy = o.deepCopy();
JsonNode species = o.get("species");
if (species != null) {
Class<? extends Animal> clazz;
switch (species.asText()) {
case "Dog":
clazz = Dog.class;
break;
case "Cat":
clazz = Cat.class;
break;
default:
return NOT_HANDLED;
}
JsonParser parser = new TreeTraversingParser(copy, p.getCodec());
parser.nextToken(); // without this an error is thrown about missing "breed" type
return ctxt.readValue(parser, clazz);
}
return NOT_HANDLED;
}
});
I believe there's probably a better way to find the typed class (I noticed that the there is a cache in one of the inputs to the handleMissingInstantiator method that contains all of the relevant types, that can probably be used to find the type based on name instead of hardcoding values as I'm doing.
serialize in json result?
How do I serialize or field the interface in json result. I using com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(obj).
The interface field not showing in json result
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#Type(value = TranslateEN.class, name = "en"),
#Type(value = TranslatePT.class, name = "pt")
})
public interface ITranslate extends Serializable {
String app = "Anota Ai";
}
#XmlRootElement
public final class TranslatePT implements ITranslate {
private static final long serialVersionUID = 1L;
private static TranslatePT instance;
private TranslatePT() {
super();
}
static {
instance = new TranslatePT();
}
public static TranslatePT getInstance() {
return instance;
}
public final Message message = new Message();
public final Entidade entidade = new Entidade();
final static class Message {
public final String defultError = "Erro inesperado";
}
final static class Login {
public final String forbidden = "Sessão expirada, favor efetuar o login novamente.";
public final String confirmacaoSenha = "A senha não confere com a confirmação de senha. Informe novamente.";
}
final static class Entidade {
public final EntidadeDeletada editada = new EntidadeDeletada();
}
final static class EntidadeDeletada {
public final String sucesso = "{0} editada com sucesso.";
}
}
public class MessageSerialize {
#Test
public void loadHtmlFileTest() throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
TranslatePT instanceTranslate = TranslatePT.getInstance();
String dtoAsString = objectMapper.writeValueAsString(instanceTranslate);
Assert.assertNotNull(dtoAsString);
}
}
//result of serialization
{"type":"pt","message":{"defultError":"Erro inesperado"},"entidade":{"editada":{"sucesso":"{0} editada com sucesso."}}}
The app variable in the interface is not an instance field, but a constant. Fields in interfaces are implicitly public static final.
Constants are associated with the class, they are not part of an instance. Jackson only serializes object instances to JSON and hence won't serialize the statics.
As Luciano points out (implicitly) static fields are not serialized by Jackson even if you use appropriate annotations. However, Jackson will by default call all getter methods it finds and put the return values in the JSON with a field name derived from getter name. So if you must serialize a static field app just put a getter method named getApp in a class, in this case TranslatePT:
public String getApp() {
return app;
}
is it possible to have a complex inheritance structure serialized and deserialized with jackson? what are the annotations for it? for example if I had the following classes
#Inheritance
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes({
#Type(value = Dog.class,name = "dog")
#Type(value = Cat.class,name = "cat")
})
public class Animal implements Serializable{
#id
long id;
String name;
}
this of course is the parent class. I know this is correct if cat and dog do not have any inheriting classes. if I want subclasses of dogs what would I need to change in both the animal class and the dog class?
here is the second class just for reference
#JsonTypeName("dog")
public class Dog extends Animal implements Serializable{
//all my props etc here
{
how would I make a retriever class and a yorki class that inherit from both animal and dog that I could cast to either one and have jackson not freak out at me.
Multi-level polymorphic tree should not be a problem with Jackson. Here is an example of the serializing / de-serializing class hierarchy similar to what you have in your question as it stands in the Jackson wiki page.
public class JacksonPolymorphism3 {
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
public abstract static class Animal {
public final String animalField;
#JsonCreator
public Animal(#JsonProperty("animalField") String animalField) {
this.animalField = animalField;
}
}
#JsonTypeName("dog")
public static class Dog extends Animal {
public final String dogField;
public Dog(#JsonProperty("animalField") String animalField,
#JsonProperty("dogField") String dogField) {
super(animalField);
this.dogField = dogField;
}
#Override
public String toString() {
return "Dog{" +
"dogField='" + dogField + '\'' +
'}';
}
}
#JsonTypeName("husky")
public static class Husky extends Dog {
public final String huskyField;
public Husky(#JsonProperty("animalField") String animalField,
#JsonProperty("dogField") String dogField,
#JsonProperty("huskyField") String huskyField) {
super(animalField, dogField);
this.huskyField = huskyField;
}
#Override
public String toString() {
return "Husky{" +
"huskyField='" + huskyField + '\'' +
'}';
}
}
public static void main(String[] args) throws IOException {
List<Dog> dogs;
dogs = Arrays.asList(new Dog("aField", "dogField"), new Husky("hField", "dField2", "hField"));
ObjectMapper mapper = new ObjectMapper();
mapper.registerSubtypes(Dog.class, Husky.class);
TypeReference<List<Dog>> referenceType = new TypeReference<List<Dog>>() {
};
String json = mapper.writerWithDefaultPrettyPrinter().withType(referenceType).writeValueAsString(dogs);
System.out.println(json);
System.out.println(mapper.readValue(json, referenceType));
}
}
Output:
[ {
"type" : "dog",
"animalField" : "aField",
"dogField" : "dogField"
}, {
"type" : "husky",
"animalField" : "hField",
"dogField" : "dField2",
"huskyField" : "hField"
} ]
[Dog{dogField='dogField'}, Husky{huskyField='hField'}]
If it doesn't help please provide more code.