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.
Related
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.
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.
I am trying to serialize an object to json using jackson 2.6.3
I want to include type info in the serialized json. This does not work for members nested within this class.
Here is the test code.
public class Test {
#JsonSubTypes({ #JsonSubTypes.Type(value = ConcreteA.class)})
interface A {
}
#JsonTypeInfo( use=JsonTypeInfo.Id.CLASS)
class ConcreteA implements A {
}
#JsonSubTypes({ #JsonSubTypes.Type(value = ConcreteB.class)})
interface B {
}
#JsonTypeInfo( use=JsonTypeInfo.Id.CLASS)
class ConcreteB implements B {
A a;
public A getA() {
return a=new ConcreteA();
}
}
#org.junit.Test
public void testSerialisation() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
System.out.println(objectMapper.writeValueAsString(new ConcreteB()));
}
}
The json being converted by jackson is
{"#class":"Test$ConcreteB","a":{}}
Note that it does not include type info for field 'a'. The type info does get included while serializing just A.
{"#class":"Test$ConcreteA"}
UPDATE:
Here is the explanation and solution from jackson dev for the problem
https://github.com/FasterXML/jackson-databind/issues/1015
It seems you were pretty close, all you need to add is a way to tell jackson how to handle your interface type.
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "#class")
#JsonSubTypes({
#JsonSubTypes.Type(value = ConcreteA.class)})
interface A {
}
So adding the #JsonTypeInfo to your interface will add the type information in a property you can configure to be the one you expect, in this case I chose #class.
The Json output I got from my example:
{"#class":"com.company.jackson.Test$ConcreteB","a":{"#type":"Test$ConcreteA"}}
Hope it helps,
José Luis
You also have to set the visible property to true, otherwise it might still not work(my case). I will enhance one of the above answers:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "#class",
visible = true)
#JsonSubTypes({
#JsonSubTypes.Type(value = ConcreteA.class)})
interface A {
}
I have faced the same issue.
I had imported wrong ObjectMapper class.
Ideally, it should be jackson-databind.jar
com.fasterxml.jackson.databind.ObjectMapper
but I have imported jackson-mapper-asl.jar
org.codehaus.jackson.map.ObjectMapper
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.
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}