So I have a class that I want jackson to serialize.
public class MyClass<T extends MyInterface> {
private T myGeneric;
private String desc;
....
}
public interface MyInterface {
String getSomeString();
}
public class MySubClass implements MyInterface {
private String info;
....
#Override
public getSomeString() {
return "";
}
}
MyClass can have many types of other classes under it's myGeneric field.
The problem is that when I pass my JSON to the server, then Jackson throws an error: problem: abstract types either need to be mapped to concrete types, have custom deserializer, or be instantiated with additional type information.
I investigated around and mostly only found examples of how to solve jackson problems with abstract classes but none for this kind of problem. I also tried using the #JsonTypeInfo and #JsonSubTypes annotations to list what kind of classes can go under MyClass but I am not sure if what I did was just wrong or not because it is hard to find any similar examples with them and the documentation in here: was not really helpful also.
Is this kind of problem solvable with Jackson annotations or I need to write a custom serializer for my class?
I am testing the serialization like this:
String json = "myJson";
ObjectMapper mapper = new ObjectMapper();
MyClass myClass = mapper.readValue(json, MyClass.class);
Jackson can't deserialize abstract types without additional info: when you have JSON with field
"myGeneric" : { "field1" : 1, "field2" : 2}
you have no idea what is the class of the myGeneric object.
So you have two options: use #JsonTypeInfo annotation or to create custom deserializer. Example:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY, property = "#class")
private T myGeneric ;
After that, serialized myGeneric field will look something like that:
"myGeneric" : { "field1" : 1, "field2" : 2, "#class" : "com.project.MySubClass"}
Deserializer will use this info to instantiate an object of correct type
Related
I'm trying to deserialize an array of MyObject (which uses the builder pattern via Lombok with #Jacksonized) from a csv String containing a non-standard representation of a map as one of the columns.
MyObject:
#JsonPropertyOrder({
"strField",
"mapField",
})
#Getter
#Jacksonized
#Builder(toBuilder = true)
#EqualsAndHashCode
#ToString
public class MyObject {
private final String strField;
#Builder.Default
private final Map<String, Float> mapField = new HashMap<>();
}
Example csv with non-standard mapField representation:
strField,mapField
abc,"key1=2.0;key2=3.0"
I'm using a mixin to try to achieve this without rewriting the entire object:
#JsonPropertyOrder({
"strField",
"mapField",
})
public abstract class MyObjectDeserializerMixin {
#JsonDeserialize(using = StringToMapDeserializer.class)
private Map<String, Float> mapField;
}
..which, as you can see above, points to a custom deserializer:
public class StringToMapDeserializer extends JsonDeserializer<Map<String, Float>> {
#Override
public Map<String, Float> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
String csvFormattedMap = jsonParser.getText().trim();
return Arrays.stream(csvFormattedMap.split(";"))
.map(keyValue -> keyValue.split("="))
.collect(Collectors.toMap(keyValue -> keyValue[0], keyValue -> Float.parseFloat(keyValue[1])));
}
}
And, to wrap it all up, I'm configuring and using my CsvMapper like so:
CsvMapper csvMapper = new CsvMapper();
csvMapper.addMixIn(MyObject.class, MyObjectDeserializerMixin.class);
CsvSchema csvSchema = csvMapper
.schemaFor(MyObject.class)
.withHeader();
ObjectReader csvReader = csvMapper.readerFor(MyObject.class).with(csvSchema);
List<MyObject> myObjects = csvReader.<MyObject>readValues(theCsvString).readAll();
However, I'm getting the following exception:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot
construct instance of java.util.LinkedHashMap (although at least one
Creator exists): no String-argument constructor/factory method to
deserialize from String value ('key1=2.0;key2=3.0') at [Source:
(StringReader); line: 2, column: 53] (through reference chain:
myPackage.MyObject$MyObjectBuilder["mapField"])
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1432)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1062)
at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:371)
at com.fasterxml.jackson.databind.deser.ValueInstantiator.createFromString(ValueInstantiator.java:258)
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:357)
at com.fasterxml.jackson.databind.deser.std.MapDeserializer.deserialize(MapDeserializer.java:29)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeSetAndReturn(MethodProperty.java:158)
at com.fasterxml.jackson.databind.deser.BuilderBasedDeserializer.vanillaDeserialize(BuilderBasedDeserializer.java:269)
at com.fasterxml.jackson.databind.deser.BuilderBasedDeserializer.deserialize(BuilderBasedDeserializer.java:193)
at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1719)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1261)
...
The stack trace appears to be trying to use a BuilderBasedDeserializer which tries to use a MapDeserializer.java for the map so it doesn't appear to be aware of my custom deserializer. I've used a very similar workflow with a custom serializer to write the same csv, so I'm confused as to why this doesn't work. What is the next step to troubleshooting this?
When using a builder to deserialize, Jackson only considers the annotations on the builder class, not those on the actual class to deserialize. Lombok's #Jacksonized helps you by automatically copying all relevant annotations to the builder class and its setter methods.
However, Lombok can only do that with annotations statically present on the class. Any dynamic annotation coming from a mixin cannot be copied, because Lombok doesn't know about them.
You could put #JsonDeserialize onto the mapField of the actual class, so that Lombok is able to copy it to the builder. But that obviously runs contrary to the purpose of mixins.
Luckily, there is a better way. You can also add the mixin to the builder class as follows:
csvMapper.addMixIn(MyObjectBuilder.class, MyObjectDeserializerMixin.class);
Strictly speaking, you do not need the mixin on MyObject any more. But if you also serialize and there are annotations relevant to serialization in the mixin, you should add the mixin to both.
However, in your case, that's not sufficient, as you are using #Builder.Default. With that annotation, Lombok creates a field named mapField$value in the builder. Jackson will not match the field mapField (and its annotation) from your mixin to that field, as they are named differently.
You can work around that by defining and annotating the setter method in your mixin:
public abstract class MyObjectDeserializerMixin {
#JsonDeserialize(using = StringToMapDeserializer.class)
public abstract void mapField(Map<String, Float> mapField);
}
You can use the actual return type of the builder here, but void is also sufficient. As #JsonDeserialize is only for deserialization purposes, you can safely remove the mapField and its annotation from the mixin class.
Tested with Lombok 1.18.20 and Jackson 2.12.2.
I would need to convert JSON to POJO, which implements following marker interface:
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import java.io.Serializable;
#JsonTypeInfo(
use = JsonTypeInfo.Id.CLASS,
include = JsonTypeInfo.As.PROPERTY,
property = "#class"
)
public interface MyInterface extends Serializable {
}
this is pojo:
public class MyPojo implements MyInterface
{
private static final long serialVersionUID = 1L;
private String phonetype;
private String cat;
//getters + setters
}
and this is snippet code where I have to use it, but I don't know what I have to do:
String json = "{\"phonetype\":\"N95\",\"cat\":\"WP\"}";
ObjectMapper mapper = new ObjectMapper();
MyPojo myPojo = mapper.readValue(json, MyPojo.class);//here I get exception: com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class path.to.MyPojo]: missing type id property '#class'
// at [Source: (String)"{"phonetype":"N95","cat":"WP"}"; line: 1, column: 30]
When I don't implement interface into MyPojo, everything works correctly without exception, but I would need implement it.
I use library jackson-databind-2.9.8
I would like to asking you, if you have any suggestion how can I solve it or if does it exist another approach how parse Pojo implementing interface showed above. I mean something missing in my interface but I don't know what.
Thank you so much
You may try to use a custom deserializer as shown in the following link How arbitrary JSON string can be deserialized to java POJO?
I've got problem similar to this:
Kafka Deserialize Nested Generic Types
In my kafka producer I am sending object that looks like this:
public class ExternalTO implements Serializable
{
private static final long serialVersionUID = 7949808917892350503L;
private List<IExternalData> externalDatas;
public ExternalTO()
{}
}
The cornerstone is this: List<IExternalData> externalDatas.
This interface looks like:
#JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public interface IExternalData
{
String getOne();
}
In my application there can by generated multiple types of IExternalBetData interface implementations (about 10 different). In this case, for instance, my producer generated ExternalTO with inner list of ConcreteExternalData objects. Sent JSON looks like:
{
"externalDatas":
[{"#class":"com.api.external.to.ConcreteExternalData",
"one":false,
"two":false}]
}
Field #class was added because of #JsonTypeInfo annotation, and I thought that this is enough for deserializer to "understend" what type of IExternalData to use in deserialization.
Unfortunately, on the side of kafka listener I am getting the exception:
Cannot construct instance of com.api.external.to.IExternalData (no
Creators, like default construct, exist): abstract types either need
to be mapped to concrete types, have custom deserializer, or contain
additional type information
Consumer looks similar to:
#Service
public class Consumer
{
private final ObjectMapper objectMapper;
public Consumer(ObjectMapper objectMapper)
{
this.objectMapper = objectMapper;
}
#KafkaListener(topics = {"${kafka.topic}"})
public void listen(ConsumerRecord<String, String> record)
{
objectMapper.readValue(record.value(), ExternalTO.class)
}
Please, help to solve this issue with deseriatization.
The deserializer doesn't know, out of all the implementations of IExternalData, to which it should deserialize the consumer record data to. We must resolve that ambiguity.
I was able to resolve this using #JsonDeserialize annotation.
#JsonDeserialize(as = <Implementation>.class
above the declaration of the List
The solution for me was to set property to objectMapper.
ObjectMapper mapper = new ObjectMapper();
// deserializes IExternalData into certain implementation.
mapper.enableDefaultTyping();
I've got classes which use an #JsonTypeIdResolver to add a custom type field to the output. This code was working as expected. I've now added an PropertyFilter to my mapper object. This is where the #JsonTypeIdResolver stopped working. The factory is not being called anymore.
Working code:
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(outputStream,myObject);
Not working code:
ObjectMapper mapper = new ObjectMapper();
PropertyFilter myfilter=new SimpleBeanFilter() {
protected boolean include(BeanPropertyWriter writer) {
return true;
}
protected boolean include(PropertyWriter writer) {
return true;
}
}
FilterProvider filters=new SimpleFilterProvider().addFilter("myFilter",myFilter);
mapper.writer(filter).writeValue(outputStream,myObject);
As the filter is just useless (accepts anything) the output should be the same. Why does my type field not get serialized anymore?
Seems like Jackson doesn't deal with inheritance the right way. My test setup was like
#JsonTypeInfo( use = JsonTypeInfo.Id.CLASS, include = As.PROPERTY, property = "_type" )
abstract class Base {
String somefield;
...
}
class ChildA extends Base {
...
}
class ChildB extends Base {
...
}
If I write a custom serializer, which explicitely casts ChildA and ChildB to Base before serializing, it works as expected. So the basic issue is that jackson does not recognize annotations on parent objects, if not explicitly told to do so.
This is the same questions than :
Jackson JSON library: how to instantiate a class that contains abstract fields
Nevertheless its solution is not possible since my abstract class is in another project than the concrete one.
Is there a way then ?
EDIT
My architecture is as follows:
public class UserDTO {
...
private LanguageDTO lang;
}
I send that object user :
restTemplate.postForObject(this.getHttpCore().trim() + "admin/user/save/1/" + idUser, userEntity, UserDTO.class);
Then I am supposed to receive it in the function :
#RequestMapping(value = "/save/{admin}/{idUser}", method = RequestMethod.POST)
public String saveUserById(#RequestBody final UserEntity user, #PathVariable Integer idUser, #PathVariable boolean admin)
with UserEntity defined as :
public class UserEntity extends AbstractUserEntity {
...
}
public abstract class AbstractUserEntity {
...
private AbstractLanguageEntity lang;
}
I would like to know how I can specify that lang should be instantiate as LanguageEntity whereas abstract classes are in another project.
This could work assuming you can configure how the object get serialized. See the example here. Look under "1.1. Global default typing" to set the defaults to include extra information in your JSON string, basically the concrete Java type that must be used when deserializing.
Since it seems you need to do this for your Spring servlet, you would have to pass a Spring message converter as mentioned here
Then inside your custom objectMapper, you can do the necessary configuration:
public class JSONMapper extends ObjectMapper {
public JSONMapper() {
this.enableDefaultTyping();
}
}
You could probably also make it work with Mix-ins, which allow you to add annotations to classes already defined. You can see and example here. This will also need to be configured inside the objectMapper.
If you need the same functionality on your client side (REST template), you can pass the object mapper as shown here.
The easiest way to solve that issue is to add getters et setters in UserEntity but specifying a concrete class :
public LanguageEntity getLang() {
return (LanguageEntity) lang;
}
public void setLang(LanguageEntity language){
this.lang = language
}
If all that you want to achieve is to note that LanguageEntity is the implementation of AbstractLanguageEntity, you can register this mapping via module:
SimpleModule myModule = new SimpleModule())
.addAbstractTypeMapping(AbstractLanguageEntity.class,
LanguageEntity.class);
ObjectMapper mapper = new ObjectMapper()
.registerMdoule(myModule);