Jackson Polymorphic Deserialization expected START_ARRAY - java

I need to deserializa a Json like this:
{
"arrayObj1":[
{
"type":"t1",
"value":[
{
"value1":"a"
},
{
"value2":"b"
}
],
"otherInfo":"abc"
}
]
}
Thats how i try to do it:
public class ClassA{
private ArrayObj1[] arrayObj1;}
Then...
public class ArrayObj1 extends Value{
private String type;
private Value [] value;
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "type")
#JsonSubTypes(value={
#JsonSubTypes.Type(value = T1.class, name = "t1")
})
public void setValue (Value [] value){
this.value = value;
}
}
and...
public abstract class Value {}
This is the error iv got:
com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class ... etc
How to handle this? Thanks.
EDIT: This is how i deserialize:
public ClassA getObj (String jsonString) {
ClassA obj = null;
try {
obj = new ObjectMapper().readValue(jsonString, ClassA.class);
} catch (IOException e) {
e.printStackTrace();
}
return obj;
}
Full error is:
com.fasterxml.jackson.databind.JsonMappingException: Unexpected token
(START_OBJECT), expected START_ARRAY: need JSON Array to contain
As.WRAPPER_ARRAY type information for class .Value at [Source:
java.io.StringReader#4317b868; line: 1, column: 496] (through
reference chain: .ClassA["arrayObj1"]->.ArrayObj1["value"])

Given the structure you use, you actually need one more level of classes to contain external type id. So something like this:
public class ClassA {
private ValueWrapper[] arrayObj1;
}
public class ValueWrapper {
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "type")
#JsonSubTypes(value={
#JsonSubTypes.Type(value = T1.class, name = "t1")
}
public Value value;
public String otherInfo;
// or use getters/setters in addition; left as fields for brevity
}
public abstract class Value { ... }
)
However, looking at your JSON more closely, this actually would only work if your values were POJOs. If assumption is that type is for all elements of the value array, there is no automated way to do that.
You will unfortunately need to handle polymorphic types manually.

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);
}
}

com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "__type"

I'm trying to understand how to do JSON deserialization using Jackson.
My JSON looks like this:
{
"__type": "base",
"__ConfigAType": "Sona",
"sonaString": "some test string",
"allRequiredSchemas": "[\"abc\"]",
"stepName": "GL1",
"preReq": []
}
My classes look like this:
public class Step {
private String stepName;
private List<Step> preReq;
private ModuleConfig moduleConfig;
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "__type")
#JsonSubTypes({
#JsonSubTypes.Type(value = BaseConfig.class, name = BaseConfig.TYPE)
})
public interface ModuleConfig {
}
public class BaseConfig implements ModuleConfig {
public static final String TYPE = "base";
}
#JsonAutoDetect(getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY)
#JsonTypeName(SonaConfig.TYPE)
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "__ConfigAType")
#JsonSubTypes({
#JsonSubTypes.Type(value = SonaConfig.class, name = SonaConfig.TYPE)
})
public abstract class ConfigA extends BaseConfig {
public static final String TYPE = "ConfigA";
private List<String> allRequiredSchemas;
abstract Boolean execute(String input);
abstract String getConfigAType();
}
public class SonaConfig extends ConfigA {
public static final String TYPE = "SONA";
private String sonaString;
#Override Boolean execute(String input) {
return true;
}
#Override String getConfigAType() {
return "Sona";
}
}
When I try to deserialize the JSON into a Step Class I get the following error:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "__type"
I'm parsing it the following way:
Step x = new ObjectMapper().readerFor(Step.class).readValue(json);
Because the field preReq will contain a list of objects of type Step, I would ideally like it to reference another JSON blob instead of creating one giant JSON. Hence I've a class of type BaseConfig and my plan is to have a RefConfig which can read references to other JSON files.

Jackson #JsonTypeInfo property attribute assumes string value

Say I have some JSON like so
[
{
'type' : {
'value': 'B'
}
},
{
'type' : {
'value': 'C'
}
}
]
Is it possible to use jackson to use the types value property to tell jackson what polymorphic type the object is? For example, i've tried something along the lines of this without any luck
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.Property, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(name = "B", value = B.class),
#JsonSubTypes.Type(name = "C", value = C.class)
}
)
abstract class A {
private Type type;
}
#JsonTypeName(value = "B")
class B extends A {
}
#JsonTypeName(value = "C")
class C extends A {
}
class Type {
private String value;
}
Using a String nested in another object in JSON to discern the type doesn't seem to be supported by Jackson. You can always use a custom Deserializer for that sort of thing. It will look like this:
class ADeserializer extends JsonDeserializer<A> {
#Override public A deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
ObjectMapper mapper = (ObjectMapper) p.getCodec();
ObjectNode node = mapper.readTree(p);
String value = node.get("type").get("value").asText();
switch (value) {
case "B": return mapper.treeToValue(node, B.class);
case "C": return mapper.treeToValue(node, C.class);
default: return null;
}
}
}
and to use it annotate the abstract class to specify the Deserializer:
#JsonDeserialize(using = ADeserializer.class)
abstract class A {
and derived classes with empty #JsonDeserialize to avoid calling the same custom Deserializer again (StackOverflowError).
#JsonDeserialize
class B extends A {
No need for #JsonTypeName, #JsonTypeInfo and #JsonSubTypes for the above.
I had a look at a few other options like a custom JsonTypeIdResolver and this answer that extends AsPropertyTypeDeserializer but couldn't get either of these to work in your case.

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.

Categories

Resources