I have the given situtation:
This is the interface I am implementing:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME
property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = MasterDevice.class, name = "COMPUTER"),
#JsonSubTypes.Type(value = SlaveDevice.class, name = "FLASH_DRIVE"),
})
interface DeviceType{
String getName();
}
The interface is used by two enums:
public enum MasterDevice implements DeviceType{
COMPUTER("Computer");
private String name;
public MasterDevice(String name){
this.name=name;
}
#Override public String getName(){return this.name;}
}
The second one is for devices you can attach to the MasterDevice.
public enum SlaveDevice implements DeviceType{
FLASH_DRIVE("USB Drive");
private String name;
public SlaveDevice(String name){
this.name=name;
}
#Override public String getName(){return this.name;}
}
The POJO that I want to deserialize is:
public class DeviceInformation{
private DeviceType type;
}
And the json String I want to deserialize look like this:
String info = "{\"type\":\"COMPUTER\"}";
ObjectMapper mapper = new ObjectMapper();
DeviceInformation deviceInfo = mapper.readValue(info, DeviceInformation.class);
All research was proposing implementing a custom deserializer for the DeviceType which I am not keen to do since it seems so bad to maintain.
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidTypeIdException: Missing type id when trying to resolve subtype of [simple type, class DeviceType]: missing type id property '#type' (for POJO property 'type')`
It seems like Jackson searches for an type property on the DeviceType which of course it does not have. How do I tell Jackson that the Enum selection is based on the enum value (COMPUTER, FLASH_DRIVE)?
I think you're expecting too many levels to be collapsed for you simply by giving a bunch of things the same field and property names.
The JSON required for your current setup would be:
String info = "{\"type\": {\"type\": \"COMPUTER\", \"COMPUTER\": null}}";
Here, the outer "type" is for DeviceInformation, the inner "type:COMPUTER" pair are for DeviceType polymorphism of MasterDevice. And the final "COMPUTER" is to instantiate MasterDevice.COMPUTER (this last bit of weirdness feels like a bug with the Jackson implementation).
To make it more obvious what's going on, here's a simplified version with some renaming:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "type"
)
#JsonSubTypes({
#JsonSubTypes.Type(value = MasterDevice.class, name = "MASTER"),
#JsonSubTypes.Type(value = SlaveDevice.class, name = "SLAVE"),
})
interface DeviceType {
}
public enum MasterDevice implements DeviceType {
LAPTOP, SERVER;
}
public enum SlaveDevice implements DeviceType {
FLASH_DRIVE, WEBCAM;
}
public class DeviceInformation {
public DeviceType deviceType;
}
Then:
String info = "{\"deviceType\": {\"type\": \"MASTER\", \"SERVER\": null}}";
ObjectMapper mapper = new ObjectMapper();
DeviceInformation deviceInfo = mapper.readValue(info, DeviceInformation.class));
If you want something more elegant, then you'll likely need a custom serializer.
Related
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)));
}
I am quite new to Java and I am trying to deserialize the JSON using Jackson and I facing some minor issue with regards to declaring the Object/Variable type. I will provide all the codes then explain the issue for easy understanding.
I have an enum that will have the required type values:
public enum IdentifierTypeValues {
Type1,
Type2,
Type3,
//Constructor and Getter of enum values
}
Then for each of these type, I have different classes which will have different input and do a completely different type of process:
public class GenerateType1 {
private String name;
private String age;
//Getter and Setter
//Some required process based on these values
}
public class GenerateType2 {
private String address;
private String city;
private String country;
//Getter and Setter
//Some required process based on these values
}
public class GenerateType3 {
private String firstName;
private String lastName;
private String fullName;
//Getter and Setter
//Some required process based on these values
}
Now I have a wrapper class for these type of classes which will take the type based on enum and typeInfo values. I want the typeInfo values to be any of the class based type something like this:
public class TypeSyntax {
private IdentifierTypeValues indeitiferType;
private GenerateType1 / GenerateType2 / GenerateType3 identifierTypeValues;
//Here the identifierTypeValues can have the values for anytype
//How to declare a variable of any of these class type?
}
This is the class that will be used by my JSON for deserializing. I know I can add a wrapper class of those 3 types and provide that wrapper class as a type class for this. Something like this:
public class WrapperClass{
private GenerateType1 type1;
private GenerateType2 type2;
private GenerateType3 type3;
}
public class TypeSyntax{
private IdentifierTypeValues indeitiferType;
private WrapperClass identifierTypeValues;
//But using this approach will change my JSON structure which I do not want to do.
}
My JSON structure is something like this and I would like to keep it in the same way.
{
"indeitiferType":"Type1",
"identifierTypeValues":{
"name":"Batman",
"age":"2008"
}
}
Is there a way I can declare the variable of multiple type class? or any better approach to handle this by keeping the json format same? I tried searching but I am unable to search what exactly so any help would be really appriciated.
Because the type identifier exists on a different level than the other properties a wrapper class TypeSyntax needed. There are several open feature requests to add wrapping functionality to Jackson e.g. https://github.com/FasterXML/jackson-databind/issues/512
Fortunately polymorphism is supported in Jackson with #JsonTypeInfo and #JsonSubTypes annotations.
Wrapper class should look like:
public class TypeSyntax {
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.EXTERNAL_PROPERTY,
property = "identifierType")
private GenerateTypeBase identifierTypeValues;
// getters and setters (omitted for brevity)
}
GenerateTypeBase is the common parent class
#JsonSubTypes({
#JsonSubTypes.Type(value = GenerateType1.class, name = "Type1"),
#JsonSubTypes.Type(value = GenerateType2.class, name = "Type2"),
})
public abstract class GenerateTypeBase {
private String name;
private String age;
// getters and setters (omitted for brevity)
}
In this different children classes will instantiated based on the identifierType property.
The children must extend this base class:
public class GenerateType2 extends GenerateTypeBase {
// additional properties
}
In a short test it will be:
#Test
void wrapperTest() throws IOException {
ObjectMapper mapper = new ObjectMapper();
GenerateType2 a = new GenerateType2();
a.setName("Foo");
a.setAge("13");
TypeSyntax w = new TypeSyntax();
w.setIdentifierTypeValues(a);
String json = mapper.writeValueAsString(w);
System.out.println(json);
}
and the output:
{
"identifierTypeValues":
{
"name":"Foo",
"age":"13"
},
"identifierType":"Type2"
}
Deserialization
#Test
void wrapperTest() throws IOException {
ObjectMapper mapper = new ObjectMapper();
String input = "{\"identifierTypeValues\": \"name\":\"Foo\",\"age\":\"13\"},\"identifierType\":\"Type2\"}";
TypeSyntax w = mapper.readValue(new StringReader(input), TypeSyntax.class);
assertAll(
() -> assertEquals(GenerateType2.class, o.getIdentifierTypeValues().getClass()),
() -> assertEquals("13", o.getIdentifierTypeValues().getAge())
);
}
If you want more flexibility you can write custom (de)serializer and / or custom resolver. Using custom TypeIdResolver that will possible to convert identifiers to types programmatically instead of using "key-value pairs" in #JsonSubTypes
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;
}
Saying I have an interface A, I want to use custom deserializer for all classes implement interface A, So I use code below but it doesn't work, While CustomAserializer works.
So what should I do to deserialize all classes implement A using my custom deserializer.
Thanks.
module.addDeserializer(A.class, new CustomADeserializer());
module.addSerializer(A.class, new CustomASerializer())
It seems you forgot to annotate your implementation classes with #JsonDeserialize(using = ImplementationClazz.class) to indicate that the class should be used to deserialize the abstract class or interface.
The following is a simple example to deserialize an interface having multiple implementations using Jackson.
Here is my interface:
#JsonDeserialize(using = UserDeserializer.class)
public interface User {
}
One implementation of the interface:
#JsonDeserialize(as = ServiceUser.class)
public class ServiceUser implements User{
private String name;
private String role;
//constructor, setters & getters
Second implementation:
#JsonDeserialize(as = AdminUser.class)
public class AdminUser implements User {
private String role;
private String designation;
//constructor, setters & getters
And here is the deserializer:
public class UserDeserializer extends JsonDeserializer<User> {
#Override
public User deserialize(JsonParser jp, DeserializationContext context) throws IOException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = mapper.readTree(jp);
/*write your own condition*/
if (root.has("name") && root.get("name").asText().equals("XYZ")) {
return mapper.readValue(root.toString(), ServiceUser.class);
}
return mapper.readValue(root.toString(), AdminUser.class);
}
}
You may get a StackOverflowError if you don't annotate the implementation classes. All implementation classes should deserialize themselves, otherwise it will use the deserializer from the parent class which leads to a StackOverflowError.
Just in case someone need a solution to serialize and desiralize inheritance hierarchy
you can use jackson annotation in more elegant way : JsonTypeInfo and JsonSubTypes
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type")
#JsonSubTypes({
#Type(value = ServiceUser.class, name = "service"),
#Type(value = AdminUser.class, name = "admin")
})
public interface User{
// ...
}
I'm familiar with the normal polymorphic deserialization stuff where you deserialize an object based on the string value of a certain field. For instance:
#JsonSubTypes(
{
#JsonSubTypes.Type(value = LionCage.class, name = "LION"),
#JsonSubTypes.Type(value = TigerCage.class, name = "TIGER"),
}
)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
Is there any way to do basically the same thing if the "type" field of the incoming object is an integer instead of a string? So in the example above, "LION" and "TIGER" would be 1 and 2. For whatever reason, I haven't been able to figure this out.
Also, how should I have been able to figure this out? Seems like it should be something obvious.
Jackson automatically converts string to numbers and vice versa. Just use string values for numbers. Like "1" for 1 value. Just try this yourself (jackson version is 2.5.1):
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.ObjectMapper;
public class HelloWorldJacksonNumber {
public static class A extends Base {
String a;
}
public static class B extends Base {
String b;
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
#JsonSubTypes({
#JsonSubTypes.Type(value = A.class, name = "1"),
#JsonSubTypes.Type(value = B.class, name = "2")})
public static class Base {
int type;
}
public static void main (String[] args) throws Exception {
final ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.version());
System.out.println(objectMapper.writeValueAsString(new A()));
System.out.println(objectMapper.writeValueAsString(new B()));
System.out.println(objectMapper.readValue("{\"type\":\"1\"}", Base.class).getClass());
System.out.println(objectMapper.readValue("{\"type\":\"2\"}", Base.class).getClass());
}
}
Output is:
2.5.1
{"type":"1"}
{"type":"2"}
class HelloWorldJacksonNumber$A
class HelloWorldJacksonNumber$B
No, that's not an option via the annotations. The TypeIdResolver interface takes and returns strings. You could do it with a custom parser/serializer using Jackson's stream API, but that seems like a lot of work to switch it to numeric field. I would only do it if someone else's system required it. If I owned the whole thing, I would just use the setup you posted in the question.