Reputation: 3331
Lets say I have JSON of the following format:
{
"type" : "Foo"
"data" : {
"object" : {
"id" : "1"
"fizz" : "bizz"
...
},
"metadata" : {
...
},
"owner" : {
"name" : "John"
...
}
}
}
I am trying to avoid custom deserializer and attempting to deserialize the above JSON (called Wrapper.java) into Java POJOs. The "type" field dictates the "object" deserialization ie. type = foo means the deserialize the "object" field using the Foo.java. (if type = Bar, use Bar.java to deserialize the object field). Metadata/owner will always deserialize the same way using a simple Jackson annotated Java class for each. Is there a way to accomplish this using annotations? If not, how can this be done using a custom deserializer?
Upvotes: 25
Views: 53806
Reputation: 131067
Alternatively to the custom deserializer approach, you can have the following for an annotations-only solution (similar to the one described in Spunc's answer, but using type
as an external property):
public abstract class AbstractData {
private Owner owner;
private Metadata metadata;
// Getters and setters
}
public static final class FooData extends AbstractData {
private Foo object;
// Getters and setters
}
public static final class BarData extends AbstractData {
private Bar object;
// Getters and setters
}
public class Wrapper {
private String type;
@JsonTypeInfo(use = Id.NAME, property = "type", include = As.EXTERNAL_PROPERTY)
@JsonSubTypes(value = {
@JsonSubTypes.Type(value = FooData.class, name = "Foo"),
@JsonSubTypes.Type(value = BarData.class, name = "Bar")
})
private AbstractData data;
// Getters and setters
}
In this approach, @JsonTypeInfo
is set to use type
as an external property to determine the right class to map the data
property.
The JSON document can be deserialized as following:
ObjectMapper mapper = new ObjectMapper();
Wrapper wrapper = mapper.readValue(json, Wrapper.class);
Upvotes: 42
Reputation: 131067
You could use a custom deserializer that checks the type
property to parse the object
property into the most suitable class.
First define an interface that will be implemented by Foo
and Bar
classes:
public interface Model {
}
public class Foo implements Model {
// Fields, getters and setters
}
public class Bar implements Model {
// Fields, getters and setters
}
Then define your Wrapper
and Data
classes:
public class Wrapper {
private String type;
private Data data;
// Getters and setters
}
public class Data {
@JsonDeserialize(using = ModelDeserializer.class)
private Model object;
private Metadata metadata;
private Owner owner;
// Getters and setters
}
The object
field is annotated with @JsonDeserialize
, indicating the deserializer that will be used for the object
property.
The deserializer is defined as following:
public class ModelDeserializer extends JsonDeserializer<Model> {
@Override
public Model deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonMappingException {
// Get reference to ObjectCodec
ObjectCodec codec = jp.getCodec();
// Parse "object" node into Jackson's tree model
JsonNode node = codec.readTree(jp);
// Get value of the "type" property
String type = ((Wrapper) jp.getParsingContext().getParent()
.getCurrentValue()).getType();
// Check the "type" property and map "object" to the suitable class
switch (type) {
case "Foo":
return codec.treeToValue(node, Foo.class);
case "Bar":
return codec.treeToValue(node, Bar.class);
default:
throw new JsonMappingException(jp,
"Invalid value for the \"type\" property");
}
}
}
The JSON document can be deserialized as following:
ObjectMapper mapper = new ObjectMapper();
Wrapper wrapper = mapper.readValue(json, Wrapper.class);
Alternatively to this custom deserializer, consider an annotations-only approach.
Upvotes: 20
Reputation: 348
All this can be done by means of annotations.
Create an abstract superclass with the common fields like "metadata" and "owner" and their getters/setters. This class needs to be annotated with @JsonTypeInfo. It should look like:
@JsonTypeInfo(use = Id.CLASS, include = As.PROPERTY, property = "type")
With the parameter property = "type"
you specify that the class identifier will be serialized under the field type in your JSON document.
The value of the class identifier can be specified with use
. Id.CLASS
uses the fully-qualified Java class name. You can also use Id.MINIMAL_CLASS
which is an abbreviated Java class name. To have your own identifier, use Id.NAME
. In this case, you need to declare the subtypes:
@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Foo.class, name = "Foo"),
@JsonSubTypes.Type(value = Bar.class, name = "Bar")
})
Implement your classes Foo and Bar by extending from the abstract superclass.
Jackson's ObjectMapper will use the additional field "type" of the JSON document for serialization and deserialization. E. g. when you deserialise a JSON string into a super class reference, it will be of the appropriate subclass:
ObjectMapper om = new ObjectMapper();
AbstractBase x = om.readValue(json, AbstractBase.class);
// x will be instanceof Foo or Bar
Complete code example (I used public fields as shortcut to not need to write getters/setters):
package test;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import java.io.IOException;
import com.fasterxml.jackson.annotation.JsonSubTypes;
@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Foo.class, name = "Foo"),
@JsonSubTypes.Type(value = Bar.class, name = "Bar")
})
public abstract class AbstractBase {
public MetaData metaData;
public Owner owner;
@Override
public String toString() {
return "metaData=" + metaData + "; owner=" + owner;
}
public static void main(String[] args) throws IOException {
// Common fields
Owner owner = new Owner();
owner.name = "Richard";
MetaData metaData = new MetaData();
metaData.data = "Some data";
// Foo
Foo foo = new Foo();
foo.owner = owner;
foo.metaData = metaData;
CustomObject customObject = new CustomObject();
customObject.id = 20l;
customObject.fizz = "Example";
Data data = new Data();
data.object = customObject;
foo.data = data;
System.out.println("Foo: " + foo);
// Bar
Bar bar = new Bar();
bar.owner = owner;
bar.metaData = metaData;
bar.data = "A String in Bar";
ObjectMapper om = new ObjectMapper();
// Test Foo:
String foojson = om.writeValueAsString(foo);
System.out.println(foojson);
AbstractBase fooDeserialised = om.readValue(foojson, AbstractBase.class);
System.out.println(fooDeserialised);
// Test Bar:
String barjson = om.writeValueAsString(bar);
System.out.println(barjson);
AbstractBase barDeserialised = om.readValue(barjson, AbstractBase.class);
System.out.println(barDeserialised);
}
}
class Foo extends AbstractBase {
public Data data;
@Override
public String toString() {
return "Foo[" + super.toString() + "; data=" + data + ']';
}
}
class Bar extends AbstractBase {
public String data;
public String toString() {
return "Bar[" + super.toString() + "; data=" + data + ']';
}
}
class Data {
public CustomObject object;
@Override
public String toString() {
return "Data[object=" + object + ']';
}
}
class CustomObject {
public long id;
public String fizz;
@Override
public String toString() {
return "CustomObject[id=" + id + "; fizz=" + fizz + ']';
}
}
class MetaData {
public String data;
@Override
public String toString() {
return "MetaData[data=" + data + ']';
}
}
class Owner {
public String name;
@Override
public String toString() {
return "Owner[name=" + name + ']';
}
}
Upvotes: 16
Reputation: 2605
I think it is rather straight-forward. You probably have a super class that has properties for metadata
and owner
, so rather than making it truly generic, you could substitute T for your super class. But basically, you will have to parse the name of the class from the actual JSON string, which in your example would look something like this:
int start = jsonString.indexOf("type");
int end = jsonString.indexOf("data");
Class actualClass = Class.forName(jsonString.substring(start + 4, end - 2)); // that of course, is approximate - based on how you format JSON
and overall code could be something like this:
public static <T> T deserialize(String xml, Object obj)
throws JAXBException {
T result = null;
try {
int start = jsonString.indexOf("type");
int end = jsonString.indexOf("data");
Class actualClass = Class.forName(jsonString.substring(start + 4, end - 2));
JAXBContextFactory factory = JAXBContextFactory.getInstance();
JAXBContext jaxbContext = factory.getJaxBContext(actualClass);
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
// this will create Java object
try (StringReader reader = new StringReader(xml)) {
result = (T) jaxbUnmarshaller.unmarshal(reader);
}
} catch (JAXBException e) {
log.error(String
.format("Exception while deserialising the object[JAXBException] %s\n\r%s",
e.getMessage()));
}
return result;
}
Upvotes: 2