How to deserialize JSON null to a NullNode instead of Java null? - java

Note: Jackson 2.1.x.
The problem is quite simple but I could not find a solution so far. I have browsed through existing documentation etc and could not find an answer.
The base class is this:
#JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "op")
#JsonSubTypes({
#Type(name = "add", value = AddOperation.class),
#Type(name = "copy", value = CopyOperation.class),
#Type(name = "move", value = MoveOperation.class),
#Type(name = "remove", value = RemoveOperation.class),
#Type(name = "replace", value = ReplaceOperation.class),
#Type(name = "test", value = TestOperation.class)
})
public abstract class JsonPatchOperation
{
/*
* Note: no need for a custom deserializer, Jackson will try and find a
* constructor with a single string argument and use it
*/
protected final JsonPointer path;
protected JsonPatchOperation(final JsonPointer path)
{
this.path = path;
}
public abstract JsonNode apply(final JsonNode node)
throws JsonPatchException;
#Override
public String toString()
{
return "path = \"" + path + '"';
}
}
And the problematic class is this:
public abstract class PathValueOperation
extends JsonPatchOperation
{
protected final JsonNode value;
protected PathValueOperation(final JsonPointer path, final JsonNode value)
{
super(path);
this.value = value.deepCopy();
}
#Override
public String toString()
{
return super.toString() + ", value = " + value;
}
}
When I try to deserialize:
{ "op": "add", "path": "/x", "value": null }
I'd like the null value to be deserialized as a NullNode, not Java null. And I couldn't find a way to do it so far.
How do you achieve that?
(note: all constructors of concrete classes are #JsonCreators with the appropriate #JsonProperty annotations -- they work without problem, my only problem is JSON null handling)

OK, well, I haven't read the documentation enough, and it is in fact very simple.
There is a JsonNodeDeserializer implementation of JsonDeserializer, which you can extend. And JsonDeserializer has a.getNullValue() method.
So, a custom deserializer was in order:
public final class JsonNullAwareDeserializer
extends JsonNodeDeserializer
{
#Override
public JsonNode getNullValue()
{
return NullNode.getInstance();
}
}
And, in the problematic class:
#JsonDeserialize(using = JsonNullAwareDeserializer.class)
protected final JsonNode value;

Related

Jackson deserialization interface on multiple types

I'm experimenting some troubles with Jackson deserialization in Java. I've made 2 solutions, and I can't resolve the problem. Problem? I got my result with the property duplicated, a field it's duplicated after jackson deserialization. (My problem is exact the same as this question: Avoid duplicate field generated by JsonTypeInfo in Jackson and no one could give you an answer at the time)
First at all, I have the following class:
#JsonIgnoreProperties(ignoreUnknown = true)
public class Instance {
#JsonProperty("id")
private String id;
#JsonProperty("name")
private String name;
#JsonProperty("type")
private InstanceType type;
}
What I'm triying to do, is just instantiate an object of type 'Instance', save it and read it. And with solution 2, the object is saved with the type duplicated (type appear as array that contain 'name', 'firs_type', for example or 'second_type) depends on what I create. With solution 1, I can save the object ok, but when I try to read it, I fall on a jackson exception casting.
Solution 1:
#JsonDeserialize(using = InstanceTypeDeserializer.class)
public interface InstanceType {
String value();
}
#JsonDeserialize(as = HardInstanceType.class)
public enum HardInstanceType implements InstanceType {
FIRST_TYPE("first_type"),
SECOND_TYPE("second_type")
private String value;
HardInstanceType(String value) {
this.value = value;
}
#JsonValue
public String value() {
return value;
}
}
#JsonDeserialize(as = SoftInstanceType.class)
public enum SoftInstanceType implements InstanceType {
//.. types implementaion similar as HardInstanceType
}
public class InstanceTypeDeserializer extends JsonDeserializer<InstanceType> {
#Override
public InstanceType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
if(root.get("name").asText().equals("hard")) {
return mapper.readValue(root.toString(), HardInstanceType.class);
} else {
return mapper.readValue(root.toString(), SoftInstanceType.class);
}
}
}
The problem with this solution, is that when I try to get the data stored, and map to the class, I get the following error:
exception parsing json:
com.fasterxml.jackson.databind.JsonMappingException: class
com.fasterxml.jackson.databind.node.TextNode cannot be cast to class
com.fasterxml.jackson.databind.node.ObjectNode
(com.fasterxml.jackson.databind.node.TextNode and
com.fasterxml.jackson.databind.node.ObjectNode are in unnamed module
of loader org.springframework.boot.loader.LaunchedURLClassLoader
#1a3e8e24) (through reference chain:
java.util.ArrayList[0]->com.project.package.xxxx.Instance["type"])
Solution 2
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "name")
#JsonSubTypes({
#JsonSubTypes.Type(value = HardInstanceType.class, name = "hard") })
public interface InstanceType {
String value();
}
The problem with this solution, is that when I save the data, when I create an Instance object, and store it, in the data Stored, I get the following:
"id": "1",
"name": "hard",
"type": [
"hard",
"first_type"
]
what is not correct, in type should be store just "first_type" (what is stored with solution 1, but I can't read it haha).
Of course, Instace class is more complex and with more fields, I reduce it here, just for the example.
I need help with this, thank you very much in advance.
Finally, I could solve the problem.
I post this just in case someone else need it.
Add a property to my HardInstanceType class.
public enum HardInstanceType implements InstanceType {
FIRST_TYPE("first_type"),
SECOND_TYPE("second_type");
private String value;
public String hardTypeIdentifierSer = "hardTypeIdentifierSer";
HardInstanceType(String value) {
this.value = value;
}
#JsonValue
public String value() {
return value;
}
}
Then, in the deserializer:
public class InstanceTypeDeserializer extends JsonDeserializer {
#Override
public InstanceType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
TreeNode node = jp.readValueAsTree();
if (node.get("hardTypeIdentifierSer") != null) {
return jp.getCodec().treeToValue(node, HardInstanceType.class);
}
}

How to deserialize JsonString using ObjectMapper with Generic Type of a class

Let's say If I have a model class ResponseModel
#Setter // This one not working
public class ResponseModel<T> {
private Class<T> responseClass;
private String content; // JsonString
public <T> T getContent() throws IOException {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(content, responseClass);
}
}
What I want to do is I want to pass Generic Type to the class ResponseModel and when I call method getContent() it should return the mapped object according to responseClass
Here is an example that I want to do it
// Color Pojo
#Data
public class Color {
private String nameValue;
private String hexValue;
}
// prepare mocked content
final String content = "{\n" +
"\"nameValue\":\"red\",\n" +
"\"hexValue\":\"FFFFFF\"\n" +
"}";
// Declare ResponseModel Object
ResponseModel<Color> response = new ResponseModel<>();
response.setContent(content);
response.getContent().getNameValue(); // should return red
response.getContent().getHexValue(); // should return FFFFFF
anyone knows how to do this?
You can make a static method to deserialize an object with generic type
public class MyDeserializer {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static <T> T convertValue(String content, Class<T> contentClass) throws IOException {
Assert.notNull(content, "Content cannot be null");
Assert.notNull(contentClass, "Content class must be specified");
return objectMapper.readValue(content, contentClass);
}
}
and to test your method :
Color color = MyDeserializer.convertValue("{" +
"\"nameValue\":\"red\"," +
"\"hexValue\":\"FFFFFF\"" + "}", Color.class);
assertEquals("red", color.getNameValue());
assertEquals("FFFFFF", color.getHexValue());
In this way, you can use the deserializer for any class at runtime.
UPDATE
To make your example work you need to remove before getContent to match with generic type T from class.
public T getContent() throws IOException {
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(content, responseClass);
}
and to use your method :
responseModel.setContent("{" +
"\"nameValue\":\"red\"," +
"\"hexValue\":\"FFFFFF\"" + "}");
responseModel.setResponseClass(Color.class);
At runtime the generic type is replaced with Object, so you must specify the expected class.
I still consider that the first solution is the clean one. You are asking if it is possible to deduce T class, but it is not possible to see the type of T at runtime.
You can write an interface with JsonSubTypes to be automatically deserialized as follows:
#JsonIgnoreProperties(value = {"type"})
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = Subclass1.class, name = "SUBCLASS1"),
#JsonSubTypes.Type(value = Subclass2.class, name = "SUBCLASS2")
})
public interface DeserializableModelInterface {
}
and write your ResponseModel with this specific object
public class ResponseModel<T extends AbstractDeserializer> {
private T body;
public ResponseModel(T body) {
this.body = body;
}
public T getBody() {
return body;
}
}
Your body is a specific object that you can get at runtime without converting json explicitly
In your Subclass1, Subclass2 and so on, you will have an additional json property that will permit to jackson to de/serialize automatically
{
"type": "SUBCLASS1"
... other properties
}
To Avoid manual mapping for subclass value, you can use class name for example:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "class")

Jackson deserialize based on type

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?
Annotations-only approach
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);
Custom deserializer approach
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.
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 + ']';
}
}
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;
}

Using Jackson to / from JSON where each class implements the same interface

I'm trying to deserialize a fairly complex JSON structure using Jackson.
Serializing works fine:
Param interface:
public interface Param<T> {
}
Not.java:
public class Not implements Param<Param<?>> {
private Param not;
public Not(){
}
public Not(Param not) {
this.not = not;
}
public Param getNot() {
return not;
}
}
And.java:
public class And implements Param<List<?>> {
private List<Param> and;
public List<Param> getAnd() {
return and;
}
public List<Param> add(Param ... params){
for (Param param : params){
this.and.add(param);
}
return this.and;
}
public And() {
this.and = new ArrayList<>();
}
public And(Param ... params){
this.and = new ArrayList<>();
for (Param param : params){
this.and.add(param);
}
}
}
Company.java:
public class CompanyName implements Param<String> {
private String companyName;
public CompanyName(String value) {
this.companyName = value;
}
public String getCompanyName() {
return companyName;
}
}
Serializing:
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(new And(new Or(new CompanyName("ABC"), new CompanyName("DEF")), new Not(new CompanyName("GHI")))));
Prints:
{"and":[{"or":[{"companyName":"ABC"},{"companyName":"DEF"}]},{"not":{"companyName":"GHI"}}]}
Now deserializing, how does Jackson know how to map and / or / companyName / not back to their objects?
And and = mapper.readValue(json, And.class);
Exception in thread "main"
com.fasterxml.jackson.databind.JsonMappingException: Can not construct
instance of com.ahp.messaging.param.Param, problem: abstract types
either need to be mapped to concrete types, have custom deserializer,
or be instantiated with additional type information at [Source:
{"and":[{"or":[{"companyName":"ABC"},{"companyName":"DEF"}]},{"not":{"companyName":"GHI"}}]};
line: 1, column: 9] (through reference chain:
com.ahp.messaging.param.And["and"]->java.util.ArrayList[0])
To get rid of the exception, I modified the Param interface as follow:
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "T", visible = false)
#JsonSubTypes({
#JsonSubTypes.Type(value = Not.class, name = "not"),
#JsonSubTypes.Type(value = And.class, name = "and"),
#JsonSubTypes.Type(value = Or.class, name = "or"),
#JsonSubTypes.Type(value = CompanyName.class, name = "companyName")
})
public interface Param<T> {
}
Now it serializes to this:
{"T":"and","and":[{"T":"or","or":[{"T":"companyName","companyName":"ABC"},{"T":"companyName","companyName":"DEF"}]},{"T":"not","not":{"T":"companyName","companyName":"GHI"}}]}
which deserializes perfectly, but there's type information on everything, is there a way to get rid of the type information and only have it where it's really needed?
Short answer, the type information is needed.
With XML you get the type information with every element, with JSON, you sacrifice the type information for smaller representations, but in this instance, scrapping the type information means there's no way to infer what class it should map to.

What #JsonTypeInfo.ID to choose for property = "type.id" for deserialization, JsonTypeInfo.Id.CUSTOM?

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?
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:
Jackson Tree Model Example.
Convert Java Object to JsonNode in Jackson.

Categories

Resources