jackson deserialize json to object without annotation - java

I have old Pojos inside jar. Example;
public class human implements Serializable {
}
public class Man extend human {
}
public class Woman extend human {
}
I have json like {"type":"man",...}
I want to deserialize proper class using type But I cannot use these annotations because Base class generated automatically from xsd.
#JsonTypeInfo(// use = JsonTypeInfo.Id.NAME, // include =
JsonTypeInfo.As.EXISTING_PROPERTY, // property = "queryType", //
visible = true) #JsonSubTypes({ // #Type(value =
ScenarioByCountryQuery.class, name = "scenarioByCountry"), //
#Type(value = ScenarioByMeasureQuery.class, name =
"scenarioByMeasure") })
I don want to desearlize like if type = '' etc.
I want to deserialize dynamically using this type value.

Related

Serialize/Deserialize using JsonTypeInfo

My goal is to convert a JSON string field to the right class using Jackson.
I have the following class:
public class AnimalRecord {
private String id;
private String source;
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "source", include = JsonTypeInfo.As.EXTERNAL_PROPERTY)
#JsonSubTypes(value = {
#JsonSubTypes.Type(value = CatProbeMetadata.class, name
= "catProbeMetadata"),
#JsonSubTypes.Type(value = DogProbeMetadata.class, name = "dogProbeMetadata"),
})
private AnimalMetadata metadata;
In addition to this class, I have a DB table where I store records of AnimalRecord (AnimalRecord = row). The AnimalMetadata is a different JSON string based on the source of this class. Each source has it's own metadata and class definition. In this example, CatProbeMetadata class will be the output when doing de-serialization from the string when the source is "cat".
The issue is that I'm not sure what to do when reading the rows from the DB. I have the following method:
private class ActiveProbeWrapper implements RowMapper<ActiveProbeRecord> {
#Override
public ActiveProbeRecord mapRow(ResultSet rs, int rowNum) throws SQLException {
String id= rs.getString("id");
String source= rs.getString("source");
Animalmetadata metadata = // NOT SURE WHAT TO DO HERE;
ActiveProbeRecord record = new ActiveProbeRecord(deviceId,segment, source, metadata);
return record;
}
}
I need to convert the string in the DB to the right class instance, but my metadata string will NOT include the source (since it's outside the metadata JSON).
The question: Will I have to add the "source" field to the metadata itself or is there any better way of doing this that I missed?
Updated example:
Example of DB rows:
id | source | metadata
1 | catSource | {"catName": "Mewy"}
2 | dogSource | {"dogName": "Barky"}
When I read the rows from the DB I want to use the source field to de-serialize metadata to the right class - String --> CatMetadata
Jackson 2.12 introduced a
new feature for type deduction :
#JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION)
#JsonSubTypes({
#JsonSubTypes.Type(DogMetadata.class),
#JsonSubTypes.Type(CatMetadata.class) })
public abstract class AnimalMetadata {
}
and therefore:
AnimalMetadata metadata = om.readValue("{\"catName\": \"Paws\"}", AnimalMetadata.class);
assertThat(metadata).isInstanceOf(CatMetadata.class);
The downside is that it might break if Jackson can't figure out which subtype to use based solely on properties names.
With this solution, optional json fields (like an absent catName property), or too similar subtypes may rise problems. #Sergei solutions doesn't have these issues (also, his solution makes use of the source field, which was your requirement).
On a side note, if you're working with SpringBoot, upgrading jackson is a matter of adding this property in pom.xml
<jackson-bom.version>2.12.3</jackson-bom.version>
The property attribute of the #JsonTypeInfo annotation marks the property that defines the entity subclass, and include = JsonTypeInfo.As.EXTERNAL_PROPERTY means that this property should be included not inside the metadata value, but on an upper level, as a property of the AnimalRecord class. This will only work if you parse the string as the AnimalRecord class.
This property should contain the value catProbeMetadata for cats and dogProbeMetadata of dogs, otherwise Jackson won't know how to parse the contents of your source field. The property may be also included inside the source string itself, but then you have to use include = JsonTypeInfo.As.PROPERTY.
Approach 1 - type is inside the metadata
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = CatProbeMetadata.class, name = "catProbeMetadata"),
#JsonSubTypes.Type(value = DogProbeMetadata.class, name = "dogProbeMetadata"),
})
class AnimalMetadata {
private String type;
}
class CatProbeMetadata extends AnimalMetadata {
private String catName;
}
class DogProbeMetadata extends AnimalMetadata {
private String dogName;
}
class AnimalRecord {
private AnimalMetadata metadata;
}
Then you could parse it as follows:
ObjectMapper mapper = new ObjectMapper();
AnimalRecord catRecord = new AnimalRecord();
catRecord.setMetadata(mapper.readValue("{\"type\":\"catProbeMetadata\",\"catName\": \"Paws\"}", AnimalMetadata.class));
AnimalRecord dogRecord = new AnimalRecord();
dogRecord.setMetadata(mapper.readValue("{\"type\":\"dogProbeMetadata\",\"dogName\": \"Fido\"}", AnimalMetadata.class));
Approach 2 - type is outside of metadata
Just select the class manually, based on the type. You don't need any annotations:
class AnimalMetadata {
}
class CatProbeMetadata extends AnimalMetadata {
private String catName;
}
class DogProbeMetadata extends AnimalMetadata {
private String dogName;
}
class AnimalRecord {
private String type;
private AnimalMetadata metadata;
}
Then you can parse like this. Putting the selection logic inside a separate method has exactly same consequences as putting it into annotations - you just need to update a different piece of code if you want to add a new subclass:
public Class<? extends AnimalMetadata> getMetadataClass(AnimalRecord record) {
switch (record.getType()) {
case "cat":
return CatProbeMetadata.class;
case "dog":
return DogProbeMetadata.class;
default:
throw new UnsupportedOperationException();
}
}
public void parse() {
ObjectMapper mapper = new ObjectMapper();
AnimalRecord catRecord = new AnimalRecord();
catRecord.setType("cat");
catRecord.setMetadata(mapper.readValue("{\"catName\": \"Paws\"}", getMetadataClass(catRecord)));
AnimalRecord dogRecord = new AnimalRecord();
dogRecord.setType("dog");
dogRecord.setMetadata(mapper.readValue("{\"dogName\": \"Fido\"}", getMetadataClass(dogRecord)));
}

How to learn Jackson to cast inheritors of abstract class?

I have a class:
#EqualsAndHashCode(callSuper = true)
#Data
public class AppealTemplateDto extends AbstractDto {
private List<AbstractFieldDto> fields;
}
This class contains list of AbstractFieldDto inheritors, e.g.:
#EqualsAndHashCode(callSuper = true)
#Data
#NoArgsConstructor
public class InputFieldDto extends AbstractFieldDto {
private String fieldType = FieldType.INPUT.name();
private String text;
}
Totally, there are near 6-7 inheritors, & AbstractTemplateDto may contain any set of them.
Controller:
#PostMapping
public ResponseEntity<AppealTemplateDto> create(#RequestBody AppealTemplateDto dto) {
return ResponseEntity.ok(service.save(dto));
}
When Jackson trying to parse AppealTemplateDto, it crashes with exception:
Caused by:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of
ru.appeal.template.dto.field.AbstractFieldDto
(no Creators, like default construct, exist): abstract types either
need to be mapped to concrete types, have custom deserializer, or
contain additional type information
As I understand, Jackson can't define, how to cast incoming AbstractFieldDto. Please, advice me, what to do?
The Annotation your are needing are:
#JsonTypeInfo
#JsonSubType
#JsonTypeName
Some explanation: if you have many implementation of your abstract type, Jackson can't guess which type is your json, you need to add a type name in json, for example as a new property (this is one of the strategies):
//tell to jackson where to find the type name
#JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = As.PROPERTY, property = "type")
// tell to jackson the implementations to scan
#JsonSubTypes({
#JsonSubTypes.Type(value = InputFieldDto.class, name = "input")
//, ...
})
public class AbstractFieldDto {
}
//tell to jackson what is the type name in json
#JsonTypeName("input")
public class InputFieldDto extends AbstractFieldDto {
private String fieldType = FieldType.INPUT.name();
private String text;
}

Missing field when deserializing using Jackson (Polymorphic)

I am trying to create an java SDK for a front-end library that takes JSON input. Essentially, this SDK converts objects of certain type into JSON which is then consumed by that front-end library.
I am using jackson's polymorphic serialization/deserialization using its annotation system.
I have a base class A and 2 child classes B and C extending A. Class A has a type field, using which I decide what class (B or C) is to be used. The syntax looks something like this:
#JsonTypeInfo({
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property= "type"
})
#JsonSubTypes({
#JsonSubTypes.Type(value = B.class, name = "b"),
#JsonSubTypes.Type(value = C.class, name = "c")
})
public class A {
private String type;
public void setType(String type){
this.type = type;
}
public String getType(){
return this.type;
}
}
public class B extends A {
}
public class C extends A {
}
So now, when I use Jackson's ObjectMapper's readValue function and read the stringified JSON and convert to class A, I get the correct instance of either class A or class B based on the value of the type variable. However, and here is the actual problem, when I try to use the function getType I always get null in those objects. I am not sure why jackson is not setting those values on the object.
String json = "{ type: 'b' }"; // example input json
ObjectMapper om = new ObjectMapper();
A a = om.readValue(json, A.class);
// a is actually an instance of class B
a.getType()// this is null
You need to add to #JsonTypeInfo the parameter visible = true to avoid to remove the type when deserializing.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property= "type",
visible = true
)
public abstract boolean visible() default false
Property that defines whether type identifier value will be passed as part of JSON stream to deserializer (true), or handled and
removed by TypeDeserializer (false). Property has no effect on
serialization. Default value is false, meaning that Jackson handles
and removes the type identifier from JSON content that is passed to
JsonDeserializer.

Jackson - #JsonTypeInfo property is being mapped as null?

I have this response:
{
"id":"decaa828741611e58bcffeff819cdc9f",
"statement":"question statement",
"exercise_type":"QUESTION"
}
Then, based on exercise_type attribute, I want to instantiate different objects instances (subclasses of ExerciseResponseDTO), so I create this mix in:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "exercise_type")
#JsonSubTypes({
#Type(value = ExerciseChoiceResponseDTO.class, name = "CHOICE"),
#Type(value = ExerciseQuestionResponseDTO.class, name = "QUESTION")})
public abstract class ExerciseMixIn
{}
public abstract class ExerciseResponseDTO {
private String id;
private String statement;
#JsonProperty(value = "exercise_type") private String exerciseType;
// Getters and setters
}
public class ExerciseQuestionResponseDTO
extends ExerciseResponseDTO {}
public class ExerciseChoiceResponseDTO
extends ExerciseResponseDTO {}
So I create my ObjectMapper as follows
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(ExerciseResponseDTO.class, ExerciseMixIn.class);
My test:
ExerciseResponseDTO exercise = mapper.readValue(serviceResponse, ExerciseResponseDTO.class)
Assert.assertTrue(exercise.getClass() == ExerciseQuestionResponseDTO.class); // OK
Assert.assertEquals("decaa828741611e58bcffeff819cdc9f" exercise.getId()); // OK
Assert.assertEquals("question statement", exercise.getStatement()); // OK
Assert.assertEquals("QUESTION", exercise.getExerciseType()); // FAIL. Expected: "QUESTION", actual: null
The problem is that, for some reason, the exercise_type attribute being used as property on #JsonTypeInfo is being mapped as null.
Any idea how i can solve this?
Finally, I've found the solution in the API Doc
Note on visibility of type identifier: by default, deserialization
(use during reading of JSON) of type identifier is completely handled
by Jackson, and is not passed to deserializers. However, if so
desired, it is possible to define property visible = true in which
case property will be passed as-is to deserializers (and set via
setter or field) on deserialization.
So the solution was simply adding the 'visible' attribute as follows
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "exercise_type",
visible = true)
#JsonSubTypes({
#Type(value = ExerciseChoiceResponseDTO.class, name = "CHOICE"),
#Type(value = ExerciseQuestionResponseDTO.class, name = "QUESTION")})
public abstract class ExerciseMixIn
{}
As per #jscherman answer by setting, 'visible' true in JsonTypeInfo will help in accessing exercise_type as a field.
If you use the same class to serialize also then resulting JSON will have exercise_type appear twice. So it's better to also update include to JsonTypeInfo.As.EXISTING_PROPERTY
And it's also worth looking at all other options for include.

Jackson : Losing type information

I am trying to serialize/deserialize the following
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
#JsonSubTypes({
#JsonSubTypes.Type(value = IdBundleCombine.class),
#JsonSubTypes.Type(value = IdBundleDistinct.class) })
public abstract class IdBundle
{
String sharedId;
Long internalId;
//getters
}
public class IdBundleCombine extends IdBundle
{
//setters
}
public class IdBundleDistinct extends IdBundle
{
//setters
}
with the following code
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(new File("foo.json"), someInstanceOfIdBundle);
Which produce the following (without type information as you can see):
{"sharedId":"foobar","internalId":1234}
So i get an error missing property '#type' that is to contain type id when I try to deserialize it.
I tried every combination of parameters for #JsonTypeInfo and #JsonSubTypes I could find without ever succeeding in getting the type information to show in my file. I also tried to play with the #JsonTypeName on the subType without results.
My only guess is that I am doing something wrong with the mapper, but I can't find anything on the subject since most of the people seem to either don't want the type information to show up in the json string, or to have problems with the deserialization process.
I did try using the following annotation and it worked, even with the property tag.
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.PROPERTY, property = "#type")
Add 'name' attribute to #Type for sub types and give 'property' attribute of #JsonTypeInfo any value of your choice. Class below
<!-- language: java -->
#JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "meta-type")
#JsonSubTypes({#Type(value = IdBundleCombine.class, name = "bundle-combine"),
#Type(value = IdBundleDistinct.class, name = "bundle-distinct")})
public abstract class IdBundle{
}
will produce following json in case it's IdBundleCombine
{"meta-type": "bundle-combine", "sharedId":"foobar","internalId":1234}

Categories

Resources