How to deserizlize json to polimorphic class? - java

I need deserialize JSON to class depends on type of one of the property.
I have the following JSON:
{
"type":"text",
"message": "Hello"
}
and I have the following Enum:
public enum MyEnumType {
TEXT("text"),
DATE("date")
private final String type;
MyEnumType(String type) {
this.type = type;
}
}
and I have the Abstract class shown as the following:
public abstract class MyClass {
public MyEnumType type;
}
and I have some of classes which extends MyClass
public class TextMessage extends MyClass {
public String message;
}
public class DateMessage extends MyClass {
...
}
and I need to write some code looks like the following:
ObjectMapper mapper = new ObjectMapper();
MyClass instance = mapper.readValue(json, MyClass.class);
and have to get instance depends on type property, if type is TEXT to deserialize to TextMessage class otherwise to DateMessage class.
How can I do this? Can you give me some ideas or examples?

You approach is a little bit confuse but maybe you could do something similar to this:
public class Test {
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
String json = "{\"type\":\"text\", \"message\": \"Hello\"}";
// create ObjectMapper instance
ObjectMapper objectMapper = new ObjectMapper();
MainObj mo = objectMapper.readValue(json, MainObj.class);
System.out.println("type: " + mo.getType());
MyClass instance = null;
if (mo.getType().equalsIgnoreCase("text")) {
// Do it for the 'text'
} else {
// Do it for the 'date'
}
}
}
class MainObj {
private String type = "";
private Object message = null;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public Object getMessage() {
return message;
}
public void setMessage(Object message) {
this.message = message;
}
}
class MyClass {
public MyClass() {
}
}

Related

Deserialize multiple json fields into single java property

I want to convert a json into Java class by having custom deserializer.
I'm able to serialize ACC_NUM, NAME and any other fields from json but not sure what can be done to convert MOBILE_NUMBER_1,MOBILE_NUMBER_2 like fields into single JSONArray(See AccountInfo class). There can be many more fields like this and count also is not fixed. Example there can be ADDRESS_1, ADDRESS_2 till ADDRESS_20 and so on and all this fields should go in JSONArray of ADDRESS after deserilization.
I have a Map of Map which holds info like this:
{
"accountInfo": {
"ACC_NUM": "1234567890",
"NAME": "John Cena",
"MOBILE_NUMBER_1": "12376534",
"MOBILE_NUMBER_2": "12376534",
"MOBILE_NUMBER_3": "12376534",
"MOBILE_NUMBER_4": "12376534"
},
"someOther": {
//similer to above
}
}
This info I want to convert to the following class CommonInfo:
public class CommonInfo {
private AccountInfo accountInfo;
//other properties...
}
#JsonIgnoreProperties(ignoreUnknown = true)
public class AccountInfo {
#JsonProperty("ACC_NUM")
private FieldValue<BigInteger> accountNum;
#JsonProperty("NAME")
private FieldValue<String> name;
#JsonProperty("MOBILE_NUMBER")
private FieldValue<JSONArray> mobileNumber;
}
//FieldValue class
public interface FieldValue<T> {
T getInitialValue();
void setInitialValue(T initialValue);
T getValue();
void setValue(T value);
}
#JsonInclude(JsonInclude.Include.ALWAYS)
public class FieldValueImpl<T> implements FieldValue<T> {
protected T initialValue;
protected T value;
//getters, setters, cons..
}
My service code takes json/Map and tries to convert it to CommonInfo class
#Service
public class MyService {
private final ObjectMapper jsonMapper = new ObjectMapper();
#PostConstruct
protected void init() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(FieldValue.class, new FieldValueSerializer());
simpleModule.addDeserializer(FieldValue.class, new FieldValueDeserializer());
jsonMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
jsonMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
jsonMapper.registerModule(simpleModule);
}
public CommonInfo setPojoResult(Map<String, LinkedHashMap<String, String>> contentAsMap) {
return jsonMapper.convertValue(contentAsMap, CommonInfo.class);
}
}
Serializer and Deserializer looks like this:
public class FieldValueDeserializer extends JsonDeserializer<FieldValue<?>> implements ContextualDeserializer {
private JavaType valueType;
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
var deserializer = new FieldValueDeserializer();
if (property == null) {
deserializer.valueType = ctxt.getContextualType().containedType(0);
} else {
var wrapperType = property.getType();
var valueType = wrapperType.containedType(0);
deserializer.valueType = valueType;
}
return deserializer;
}
#Override
public FieldValue<?> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
FieldValueDeserializer deserializer = new FieldValueDeserializer();
deserializer.getKnownPropertyNames();
FieldValue<?> fieldValueImpl = new FieldValueImpl<>();
if (valueType.toString().contains("java.time.LocalDate")) {
JsonNode node = parser.getCodec().readTree(parser);
FieldValue<LocalDate> f1 = new FieldValueImpl<>();
f1.setValue(DateUtils.convertJulianToLocalDate(node.textValue()));
return f1;
} else {
fieldValueImpl.setValue(context.readValue(parser, valueType));
}
return fieldValueImpl;
}
}
//--
public class FieldValueSerializer extends StdSerializer<FieldValue> {
public FieldValueSerializer() {
this(null);
}
public FieldValueSerializer(Class<FieldValue> vc) {
super(vc);
}
#Override
public void serialize(FieldValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(String.valueOf(value.getCurValue()));
}
}

Deserialize a JSON payload to object base on JSON integer property

I have below classes:
public class Result<T> {
public int code;
public Object meta;
public T data;
}
public class User {
public int id;
public String name;
}
public class Error {
public String field;
public String message;
}
I want to deserialize a JSON payload based on code field. If code >= 10, return Result<ArrayList<Error>>, otherwise return Result<User>
Currently, I map JSON to Result<Object> first, then check the code field. Based on that value I make second map to desired object.
ObjectMapper mapper = new ObjectMapper();
Result<Object> tempResult = mapper.readValue(json, new TypeReference<Result<Object>>() {});
if (tempResult.code < 10) {
Result<User> result = mapper.readValue(json, new TypeReference<Result<User>>() {});
return result;
} else {
Result<ArrayList<Error>> result = mapper.readValue(json, new TypeReference<Result<ArrayList<Error>>>() {});
return result;
}
Is there an elegant way to do this without deserializing it 2 times?
You need to implement custom TypeIdResolver:
class UserTypeIdResolverBase extends TypeIdResolverBase {
#Override
public String idFromValue(Object value) {
throw new IllegalStateException("Not implemented!");
}
#Override
public String idFromValueAndType(Object value, Class<?> suggestedType) {
throw new IllegalStateException("Not implemented!");
}
#Override
public JsonTypeInfo.Id getMechanism() {
return JsonTypeInfo.Id.CUSTOM;
}
#Override
public JavaType typeFromId(DatabindContext context, String id) {
if (Integer.parseInt(id) < 10) {
return context.getTypeFactory().constructType(new TypeReference<Result<User>>() {});
}
return context.getTypeFactory().constructType(new TypeReference<Result<List<Error>>>() {});
}
}
and declare it for a Result class:
#JsonTypeInfo(property = "code", use = JsonTypeInfo.Id.CUSTOM, visible = true)
#JsonTypeIdResolver(UserTypeIdResolverBase.class)
class Result<T>

Jackson Polymorphism: Nested Subtypes

Is it possible to use multiple #JsonSubType annotations in a nested fashion?
For example, imagine the following classes:
#Data
#JsonSubTypeInfo(include=As.EXISTING_PROPERTY, property="species", use=Id.NAME, visible=true)
#JsonSubTypes({
#Type(name="Dog", value=Dog.class)
#Type(name="Cat", value=Cat.class)
})
public abstract class Animal {
private String name;
private String species;
}
#Data
#JsonSubTypeInfo(include=As.EXISTING_PROPERTY, property="breed", use=Id.NAME, visible=true)
#JsonSubTypes({
#Type(name="Labrador", value=Labrador.class)
#Type(name="Bulldog", value=Bulldog.class)
})
public abstract class Dog extends Animal {
private String breed;
}
#Data
public class Cat extends Animal {
private boolean lovesCatnip;
}
#Data
public class Labrador extends Dog {
private String color;
}
#Data
public class Bulldog extends Dog {
private String type; // "frenchy", "english", etc..
}
If I use an object mapper, I can successfully map a Bulldog to JSON, however, when trying to read the resulting JSON and read it back in, I get an error like the following:
Can not construct instance of com.example.Dog abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
Is it possible to get Jackson to work with such subtyping? Would I need to create a custom deserializer for each subclass?
EDIT:
I've modified the classes above slightly from the original posting. I added a Cat class and had that and Dog extend from Animal.
Here is a sample JSON that can be created using the ObjectMapper::writeValueAsString:
{
"name": null,
"species": "Dog",
"breed": "Bulldog",
"type": "B-Dog"
}
The following works if I use #JsonTypeInfo and a similar set up to yours. Maybe your problem is in your deserialization code, so have a look at this:
public class MyTest {
#Test
public void test() throws IOException {
final Bulldog bulldog = new Bulldog();
bulldog.setBreed("Bulldog");
bulldog.setType("B-Dog");
final ObjectMapper om = new ObjectMapper();
final String json = om.writeValueAsString(bulldog);
final Dog deserialized = om.readValue(json, Dog.class);
assertTrue(deserialized instanceof Bulldog);
}
#JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "species", use = Id.NAME, visible = true)
#JsonSubTypes({
#Type(name = "Dog", value = Dog.class),
#Type(name = "Cat", value = Cat.class)
})
public static abstract class Animal {
private String name;
private String species;
}
#JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "breed", use = Id.NAME, visible = true)
#JsonSubTypes({
#Type(name = "Labrador", value = Labrador.class),
#Type(name = "Bulldog", value = Bulldog.class)
})
public static abstract class Dog {
private String breed;
public String getBreed() {
return breed;
}
public void setBreed(final String breed) {
this.breed = breed;
}
}
public static abstract class Cat {
private String name;
}
public static class Labrador extends Dog {
private String color;
public String getColor() {
return color;
}
public void setColor(final String color) {
this.color = color;
}
}
public static class Bulldog extends Dog {
private String type; // "frenchy", "english", etc..
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
}
}
EDITed for the updated question: If you can use the same property (in the following code the hidden property "#class") for the inheritance hierarchy, it works:
#Test
public void test() throws IOException {
final Bulldog bulldog = new Bulldog();
// bulldog.setSpecies("Dog");
// bulldog.setBreed("Bulldog");
bulldog.setType("B-Dog");
final ObjectMapper om = new ObjectMapper();
final String json = om.writeValueAsString(bulldog);
final Animal deserialized = om.readValue(json, Animal.class);
assertTrue(deserialized instanceof Bulldog);
}
#JsonTypeInfo(include = As.PROPERTY, use = Id.CLASS, visible = false)
#JsonSubTypes({
#Type(Dog.class),
#Type(Cat.class)
})
public static abstract class Animal {
}
#JsonTypeInfo(include = As.PROPERTY, use = Id.CLASS, visible = false)
#JsonSubTypes({
#Type(name = "Labrador", value = Labrador.class),
#Type(name = "Bulldog", value = Bulldog.class)
})
public static abstract class Dog
extends Animal {
}
If you want to set the animal type (e.g. to compute species, breed etc.), you could also use this setup:
#Test
public void test() throws IOException {
final Bulldog bulldog = new Bulldog();
bulldog.setAnimalType("Bulldog");
// bulldog.setSpecies("Dog");
// bulldog.setBreed("Bulldog");
bulldog.setType("B-Dog");
final ObjectMapper om = new ObjectMapper();
final String json = om.writeValueAsString(bulldog);
System.out.println(json);
final Animal deserialized = om.readValue(json, Animal.class);
assertTrue(deserialized instanceof Bulldog);
}
#JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "animalType", use = Id.NAME, visible = true)
#JsonSubTypes({
#Type(Dog.class)
})
public static abstract class Animal {
private String animalType;
public String getAnimalType() {
return animalType;
}
public void setAnimalType(final String animalType) {
this.animalType = animalType;
}
}
#JsonTypeInfo(include = As.EXISTING_PROPERTY, property = "animalType", use = Id.NAME, visible = true)
#JsonSubTypes({
#Type(value = Bulldog.class)
})
public static abstract class Dog
extends Animal {
}
#JsonTypeName("Bulldog")
public static class Bulldog extends Dog {
private String type; // "frenchy", "english", etc..
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
}
I was able to solve this such that the following JSON translates to a Bulldog object:
{
"species": "Dog",
"breed": "Bulldog",
"name": "Sparky",
"type": "English"
}
I used the following code to do this:
ObjectMapper om = new ObjectMapper();
om.addHandler(new DeserializationProblemHandler() {
#Override
public Object handleMissingInstantiator(DeserializationContext ctxt, Class<?> instClass, JsonParser p, String msg) throws IOException {
JsonNode o = p.readValueAsTree();
JsonNode copy = o.deepCopy();
JsonNode species = o.get("species");
if (species != null) {
Class<? extends Animal> clazz;
switch (species.asText()) {
case "Dog":
clazz = Dog.class;
break;
case "Cat":
clazz = Cat.class;
break;
default:
return NOT_HANDLED;
}
JsonParser parser = new TreeTraversingParser(copy, p.getCodec());
parser.nextToken(); // without this an error is thrown about missing "breed" type
return ctxt.readValue(parser, clazz);
}
return NOT_HANDLED;
}
});
I believe there's probably a better way to find the typed class (I noticed that the there is a cache in one of the inputs to the handleMissingInstantiator method that contains all of the relevant types, that can probably be used to find the type based on name instead of hardcoding values as I'm doing.

Jackson json property (string) to instance

Assuming I have the following JSON:
{
"property": "123:1234"
}
How do I use Jackson annotations to ensure that the string value of "property" is de-serialized to a self-defined class rather than a String object?
I went through their documentation and I was unable to find this particular feature.
Thanks in advance.
You could create custom deserializer for your field. Assuming you want to map it to SomeClass object :
public class SomeClass {
#JsonDeserialize(using = CustomPropertyDeserializer.class)
private Properties property;
public Properties getProperty() {
return property;
}
public void setProperty(Properties property) {
this.property = property;
}
}
You annotate your field that you want to deserialize customly with #JsonDeserialize annotation passing custom deserializer.
Your deserializer could look like this :
public class CustomPropertyDeserializer extends StdDeserializer<Properties> {
public CustomPropertyDeserializer() {
super(Properties.class);
}
#Override
public Properties deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String valueAsString = p.getValueAsString();
String[] split = valueAsString.split(":");
return new Properties(split[0], split[1]);
}
}
And custom property class :
public class Properties {
private String first;
private String second;
public Properties(String first, String second) {
this.first = first;
this.second = second;
}
public String getFirst() {
return first;
}
public void setFirst(String first) {
this.first = first;
}
public String getSecond() {
return second;
}
public void setSecond(String second) {
this.second = second;
}
}
For testing it :
public static void main(String[] args) throws IOException {
String s = Files.lines(Paths.get("src/main/resources/data.json")).collect(Collectors.joining());
ObjectMapper objectMapper = new ObjectMapper();
SomeClass someClass = objectMapper.readValue(s, SomeClass.class);
System.out.println(someClass.getProperty().getFirst());
System.out.println(someClass.getProperty().getSecond());
}
The output is then :
123
1234
So all the custom logic how to map your String to some class that you define could be placed in deserialize method of your custom deserializer.
First thing first define your class that needs to be used:
#JsonInclude(JsonInclude.Include.NON_NULL)
public class JsonTest{
#JsonProperty("property")
private String property;
//define your getters and setters for the field
Then you can use the ObjectMapper class from jackson:
public static <T> T extractObjectFromJson(String jsonText, Class<T> type) {
try {
return new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).reader().forType(type)
.readValue(jsonText);
} catch (Exception e) {
//Manage your exception here
}
return null;
}
So you can just call the method extractobjectFromJson(//Your JSON String, JsonTest.class) to get your JSON deserialized.

Jackson cannot deserialize enum as object even if I add customized deserializer

I want to use Jackson JSON to serialize/deserialize a class containing an enum object. My class is:
class Bar {
#JsonProperty("rateType")
#JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
private ReturnedRateType rateType;
public ReturnedRateType getRateType() {
return rateType;
}
public void setRateType(ReturnedRateType rateType) {
this.rateType = rateType;
}
}
The enum class ReturnedRateType is defined as:
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ReturnedRateType {
AA("AA"),
BB("BB"),
CC("CC");
#JsonProperty("value")
private String value;
ReturnedRateType(String value) {
this.value = value;
}
#JsonCreator
public static ReturnedRateType fromValue(final String value) {
if (value != null) {
for (ReturnedRateType type : ReturnedRateType.values()) {
if (value.equalsIgnoreCase(type.value)) {
return type;
}
}
}
return null;
}
}
As you see, I added #JsonFormat annotation to tell Jackson to serialize this enum as POJO, and added #JsonCreator annotation to get a static factory method from given string to enum object. Since Jackson can only serialize but can't deserialize from object representation to enum, I added the following customized deserializer for the enum ReturnedRateType:
public class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {
#Override
public ReturnedRateType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString());
if(type != null)
return type;
throw new JsonMappingException("invalid value for ReturnedRateType");
}
}
But when I tested deserialization from a JSON string to enum, I got the error. The JSON string is:
{"rateType": {"value": "AA"}}
My test code is:
#Test
public void RateTypeToEnum() {
String json = "{\"rateType\": {\"value\": \"AA\"}}";
System.out.println(json);
ObjectMapper mapper = new ObjectMapper();
Bar bar = null;
try {
bar = mapper.readValue(json, Bar.class);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(bar.getRateType());
}
I expect to see the output should be AA. But jp.getValueAsString() in my customized deserializer ReturnedRateTypeDeserializer is null during the execution:
ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString()); //jp.getValueAsString() is null here!
Thus it returns error. So what is wrong here?
According to the Jackson 2.5.X documentation on the JsonFormat annotation the Shape.Object does not work for the enum deserialisation:
Enums: Shapes JsonFormat.Shape.STRING and JsonFormat.Shape.NUMBER can
be used to change between numeric (index) and textual (name or
toString()); but it is also possible to use JsonFormat.Shape.OBJECT
to serialize (but not deserialize).
I'd make the JsonCreator static method accept a JsonNode and read the string value from it.
Note that this would work since 2.5.X. In early versions you would need to write a custom deserialiser. Here is an example:
public class JacksonEnumObjectShape {
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
#JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
public enum ReturnedRateType {
AA("AA"),
BB("BB"),
CC("CC");
#JsonProperty("value")
private String value;
ReturnedRateType(String value) {
this.value = value;
}
#JsonCreator
public static ReturnedRateType fromValue(final JsonNode jsonNode) {
for (ReturnedRateType type : ReturnedRateType.values()) {
if (type.value.equals(jsonNode.get("value").asText())) {
return type;
}
}
return null;
}
}
// can be avoided since 2.5
public static class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {
#Override
public ReturnedRateType deserialize(
final JsonParser jp,
final DeserializationContext ctxt) throws IOException {
final JsonNode jsonNode = jp.readValueAsTree();
return ReturnedRateType.fromValue(jsonNode);
}
}
public static void main(String[] args) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
final String json = mapper.writeValueAsString(ReturnedRateType.AA);
System.out.println(json);
System.out.println(mapper.readValue(json, ReturnedRateType.class));
}
}
Output:
{"value":"AA"}
AA

Categories