I am consuming a "RESTful" service (via RestTemplate) that produces JSON as follows:
{
"id": "abcd1234",
"name": "test",
"connections": {
"default": "http://foo.com/api/",
"dev": "http://dev.foo.com/api/v2"
},
"settings": {
"foo": "{\n \"fooId\": 1, \"token\": \"abc\"}",
"bar": "{\"barId\": 2, \"accountId\": \"d7cj3\"}"
}
}
Note the settings.foo and settings.bar values, which cause issues on deserialization. I would like to be able to deserialize into objects (e.g., settings.getFoo().getFooId(), settings.getFoo().getToken()).
I was able to solve this specifically for an instance of Foo with a custom deserializer:
public class FooDeserializer extends JsonDeserializer<Foo> {
#Override
public Foo deserialize(JsonParser jp, DeserializationContext ctx) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
String text = node.toString();
String trimmed = text.substring(1, text.length() - 1);
trimmed = trimmed.replace("\\", "");
trimmed = trimmed.replace("\n", "");
ObjectMapper mapper = new ObjectMapper();
JsonNode obj = mapper.readTree(trimmed);
Foo result = mapper.convertValue(obj, Foo.class);
return result;
}
}
#Data
#JsonIgnoreProperties(ignoreUnknown = true)
public class Settings {
#JsonDeserialize(using = FooDeserializer.class)
private Foo foo;
private Bar bar;
}
However, now if I want to deserialize settings.bar, I need to implement another custom deserializer. So I implemented a generic deserializer as follows:
public class QuotedObjectDeserializer<T> extends JsonDeserializer<T> implements ContextualDeserializer {
private Class<?> targetType;
private ObjectMapper mapper;
public QuotedObjectDeserializer(Class<?> targetType, ObjectMapper mapper) {
this.targetType = targetType;
this.mapper = mapper;
}
#Override
public JsonDeserializer<T> createContextual(DeserializationContext context, BeanProperty property) {
this.targetType = property.getType().containedType(1).getRawClass();
return new QuotedObjectDeserializer<T>(this.targetType, this.mapper);
}
#Override
public T deserialize(JsonParser jp, DeserializationContext context) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
String text = node.toString();
String trimmed = text.substring(1, text.length() - 1);
trimmed = trimmed.replace("\\", "");
trimmed = trimmed.replace("\n", "");
JsonNode obj = this.mapper.readTree(trimmed);
return this.mapper.convertValue(obj, this.mapper.getTypeFactory().constructType(this.targetType));
}
}
Now I'm not sure how to actually use the deserializer, as annotating Settings.Foo with #JsonDeserialize(using = QuotedObjectDeserializer.class) obviously does not work.
Is there a way to annotate properties to use a generic custom deserializer? Or, perhaps more likely, is there a way to configure the default deserializers to handle the stringy objects returned in my example JSON?
Edit: The problem here is specifically deserializing settings.foo and settings.bar as objects. The JSON representation has these objects wrapped in quotes (and polluted with escape sequences), so they are deserialized as Strings.
Sorry about the length of the code here. There are plenty of shortcuts here (no encapsulation; added e to defaulte to avoid keyword etc.) but the intent is there
Model class:
package com.odwyer.rian.test;
import java.io.IOException;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Model {
public String id;
public String name;
public Connections connections;
public Settings settings;
public static class Connections {
public String defaulte;
public String dev;
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
public static class Foo {
public Foo () {}
#JsonCreator
public static Foo create(String str) throws JsonParseException, JsonMappingException, IOException {
return (new ObjectMapper()).readValue(str, Foo.class);
}
public Integer fooId;
public String token;
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
public static class Bar {
public Bar() {}
#JsonCreator
public static Bar create(String str) throws JsonParseException, JsonMappingException, IOException {
return (new ObjectMapper()).readValue(str, Bar.class);
}
public Integer barId;
public String accountId;
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
public static class Settings {
public Foo foo;
public Bar bar;
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
The caller:
package com.odwyer.rian.test;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestClass {
private static ObjectMapper objectMapper = new ObjectMapper();
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
Scanner file = new Scanner(new File("test.json"));
String jsonStr = file.useDelimiter("\\Z").next();
Model model = objectMapper.readValue(jsonStr, Model.class);
System.out.println(model.toString());
}
}
The result (too much hassle to format out but it is all there!):
com.odwyer.rian.test.Model#190083e[id=abcd1234,name=test,connections=com.odwyer.rian.test.Model$Connections#170d1f3f[defaulte=http://foo.com/api/,dev=http://dev.foo.com/api/v2],settings=com.odwyer.rian.test.Model$Settings#5e7e6ceb[foo=com.odwyer.rian.test.Model$Foo#3e20e8c4[fooId=1,token=abc],bar=com.odwyer.rian.test.Model$Bar#6291bbb9[barId=2,accountId=d7cj3]]]
The key, courtesy of Ted and his post (https://stackoverflow.com/a/8369322/2960707) is the #JsonCreator annotation
Related
This question already has answers here:
What is use of the annotation #JsonIgnore?
(3 answers)
Closed 5 months ago.
I have a class like
public class MyPojo {
String name,
String age
String sub
}
And map like
map("name":"john","age":21)
Using Jacksons ObjectMapper, I get a string like
{
"name": "john",
"age": "21",
"sub": null
}
but instead I want to exclude the sub:
{
"name": "john",
"age": "21"
}
How can I do that and tell Jackson to skip sub?
P.S. Please keep in mind that I want to have ability to exclude age and include sub without changing the POJO class, so #JsonIgnore doesn't quite fit.
You can use java.util.Optional in your POJO class. You can convert Map to POJO and after that serialise it ignoring null-s. Optional allows to distinguish map.put("property", null) from not setting property at all. See below example:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class DateApp {
private final static JsonMapper JSON_MAPPER = JsonMapper.builder()
.enable(SerializationFeature.INDENT_OUTPUT)
.addModule(new Jdk8Module())
.build();
public static void main(String[] args) throws Exception {
Map<String, Object> map = new HashMap<>();
map.put("name", "John");
map.put("age", 21);
MyPojo pojo = JSON_MAPPER.convertValue(map, MyPojo.class);
System.out.println(pojo);
System.out.println("JSON:");
JSON_MAPPER.writeValue(System.out, pojo);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonInclude(JsonInclude.Include.NON_NULL)
class MyPojo {
private Optional<String> name;
private Optional<String> age;
private Optional<String> sub;
}
Above code prints:
MyPojo(name=Optional[John], age=Optional[21], sub=null)
JSON:
{
"name" : "John",
"age" : "21"
}
You can try this approach in order to avoid the null attributes in the final json.
I have used ObjectMapper object and set the below property to avoid null attributes in the json.
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Code as follows:
MyPojo.java
public class MyPojo {
private String name;
private String age;
private String sub;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return age;
}
public void setAge(String age) {
this.age = age;
}
public String getSub() {
return sub;
}
public void setSub(String sub) {
this.sub = sub;
}
#Override
public String toString() {
return "MyPojo{" +
"name=" + name +
", age=" + age +
", sub=" + sub +
'}';
}
}
Test.java
public class Test {
public static void main(String[] args) throws JsonProcessingException {
Map<String,String> inputMap = new HashMap<>();
inputMap.put("age","21");
inputMap.put("name","John");
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
MyPojo p = mapper.convertValue(inputMap,MyPojo.class);
System.out.println(p);
System.out.println(mapper.writeValueAsString(p));
}
}
Output:
MyPojo{name=John, age=21, sub=null}
{"name":"John","age":"21"}
You can create your custom serializer.
Just include your map in the serializer code
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
#JsonSerialize(using = MyPojoSerializer.class)
public class MyPojo {
String name;
String age;
String sub;
}
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class MyPojoSerializer extends JsonSerializer<MyPojo> {
#Override
public void serialize(MyPojo myPojo, JsonGenerator jGen, SerializerProvider serializerProvider) throws IOException {
jGen.writeStartObject();
// Map map = ....
for (final Field field : myPojo.getClass().getDeclaredFields()) {
ReflectionUtils.makeAccessible(field);
final String fieldName = field.getName();
final Object fieldValue = ReflectionUtils.getField(field, myPojo);
if (map.containsKey(fieldName)) {
jGen.writeFieldName(fieldName);
jGen.writeObject(fieldValue);
}
}
jGen.writeEndObject();
}
}
I am using Jackson to serialize my Java POJO classes. In addition to fields, I have in Java POJO, I would like to add some additional information in JSON I am writing my own custom CustomClassSerializer. If I use this class and register to ObjectMapper then I get the error:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Type id handling not implemented for type org.acme.Tiger (by serializer of type org.acme.CustomModule$CustomClassSerializer)
I am unable to understand what might be going wrong here. If I remove the custom registered model then everything works perfectly.
Can someone please let me know what may be the cause of this issue? I am currently using Jackson 2.13.2 latest version dependencies: jackson-core, jackson-databind, jackson-annotations, jackson-datatype-jdk8:
Following is the sample code:
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.*;
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, visible = true, property = "type")
#JsonSubTypes({#JsonSubTypes.Type(value = Bat.class, name = "Bat"),
#JsonSubTypes.Type(value = Tiger.class, name = "Tiger")})
#JsonInclude(JsonInclude.Include.NON_NULL)
#Data
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class Animal {
private String type;
private String name;
}
#JsonInclude(JsonInclude.Include.NON_NULL)
#EqualsAndHashCode(callSuper = true)
#ToString(callSuper = true)
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonTypeName("Tiger")
public class Tiger extends Animal {
private String livingType;
private String foundIn;
}
#JsonInclude(JsonInclude.Include.NON_NULL)
#EqualsAndHashCode(callSuper = true)
#ToString(callSuper = true)
#Data
#NoArgsConstructor
#AllArgsConstructor
#JsonTypeName("Bat")
public class Bat extends Animal{
private String livingType;
private String foundIn;
}
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
import java.io.IOException;
public class CustomModule extends SimpleModule {
public CustomModule() {
addSerializer(Tiger.class, new CustomClassSerializer());
}
private static class CustomClassSerializer extends JsonSerializer {
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeObjectField("my_extra_field1", "some data");
jgen.writeObjectField("my_extra_field2", "some more data");
JavaType javaType = provider.constructType(Tiger.class);
BeanDescription beanDesc = provider.getConfig().introspect(javaType);
JsonSerializer<Object> serializer = BeanSerializerFactory.instance.createSerializer(provider, javaType);
serializer.unwrappingSerializer(null).serialize(value, jgen, provider);
jgen.writeEndObject();
}
}
}
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestMain {
public static void main(String[] args) throws JsonProcessingException {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.registerModule(new CustomModule());
Tiger tiger = new Tiger();
tiger.setType("Tiger");
tiger.setName("Shera");
tiger.setFoundIn("Ground");
tiger.setLivingType("Tree");
System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(tiger));
}
}
I would like to know what are the causes for the above-mentioned exception.
I don't think I can write really efficient Jackson code while being a Gson-oriented guy, but I guess you could incorporate the following Q/As to resolve your question:
https://stackoverflow.com/a/27893673/12232870 - in order to overcome your issue;
https://stackoverflow.com/a/31057934/12232870 - in order to use the default serialization strategies.
Also, I'd like to refactor your code (a lot) in order to make it flexible by using the strategy design pattern (where necessary), and demonstrate how it works using with simple unit tests.
public final class InterceptingSerializerModifier<T>
extends BeanSerializerModifier {
public interface IAppender<T> {
void append(JsonGenerator generator, T value)
throws IOException;
}
private final Class<T> baseClass;
private final IAppender<? super T> appender;
private InterceptingSerializerModifier(final Class<T> baseClass, final IAppender<? super T> appender) {
this.baseClass = baseClass;
this.appender = appender;
}
public static <T> BeanSerializerModifier create(final Class<T> baseClass, final IAppender<? super T> appender) {
return new InterceptingSerializerModifier<>(baseClass, appender);
}
#Override
public JsonSerializer<?> modifySerializer(final SerializationConfig config, final BeanDescription description, final JsonSerializer<?> serializer) {
if ( !baseClass.isAssignableFrom(description.getBeanClass()) ) {
return serializer;
}
#SuppressWarnings("unchecked")
final JsonSerializer<? super T> castSerializer = (JsonSerializer<? super T>) serializer;
return InterceptingJsonSerializer.create(castSerializer, appender);
}
private static final class InterceptingJsonSerializer<T>
extends JsonSerializer<T> {
private static final NameTransformer identityNameTransformer = new NameTransformer() {
#Override
public String transform(final String name) {
return name;
}
#Override
public String reverse(final String transformed) {
return transformed;
}
};
private final JsonSerializer<? super T> unwrappedSerializer;
private final IAppender<? super T> appender;
private InterceptingJsonSerializer(final JsonSerializer<? super T> unwrappedSerializer, final IAppender<? super T> appender) {
this.unwrappedSerializer = unwrappedSerializer;
this.appender = appender;
}
private static <T> JsonSerializer<T> create(final JsonSerializer<? super T> serializer, final IAppender<? super T> appender) {
return new InterceptingJsonSerializer<>(serializer.unwrappingSerializer(identityNameTransformer), appender);
}
#Override
public void serializeWithType(final T value, final JsonGenerator generator, final SerializerProvider provider, final TypeSerializer serializer)
throws IOException {
serializer.writeTypePrefix(generator, serializer.typeId(value, JsonToken.START_OBJECT));
doSerialize(value, generator, provider);
serializer.writeTypeSuffix(generator, serializer.typeId(value, JsonToken.START_OBJECT));
}
#Override
public void serialize(final T value, final JsonGenerator generator, final SerializerProvider provider)
throws IOException {
generator.writeStartObject();
doSerialize(value, generator, provider);
generator.writeEndObject();
}
private void doSerialize(final T value, final JsonGenerator generator, final SerializerProvider provider)
throws IOException {
unwrappedSerializer.serialize(value, generator, provider);
appender.append(generator, value);
}
}
}
public final class InterceptingSerializerModifierTest {
private static final BeanSerializerModifier unit = InterceptingSerializerModifier.create(
IValue.class,
(generator, value) -> {
if ( value instanceof Foo foo ) {
generator.writeObjectField("#extra.message", "this is from foo: " + foo.value);
} else if ( value instanceof Bar bar ) {
generator.writeObjectField("#extra.message", "this is from bar: " + bar.value);
} else {
generator.writeObjectField("#extra.message", "something else...");
}
}
);
private static final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new SimpleModule()
.setSerializerModifier(unit)
);
private static Stream<Arguments> test() {
return Stream.of(
Arguments.of(
"{\"#type\":\"FOO\",\"value\":1,\"#extra.message\":\"this is from foo: 1\"}",
new Foo(1)
),
Arguments.of(
"{\"#type\":\"BAR\",\"value\":2.0,\"#extra.message\":\"this is from bar: 2.0\"}",
new Bar(2)
)
);
}
#ParameterizedTest
#MethodSource
public void test(final String expectedJson, final IValue actualValue)
throws JsonProcessingException {
Assertions.assertEquals(expectedJson, objectMapper.writeValueAsString(actualValue));
}
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
#JsonSubTypes({
#JsonSubTypes.Type(value = Foo.class, name = "FOO"),
#JsonSubTypes.Type(value = Bar.class, name = "BAR")
})
private sealed interface IValue
permits Foo, Bar {
}
#RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private static final class Foo
implements IValue {
#Getter
private final int value;
}
#RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private static final class Bar
implements IValue {
#Getter
private final double value;
}
}
I hope the code above is pretty self-explaining. You may also want to enhance the code in order to make identityNameTransformer injectable too, if you need to use extra property name transformations.
Unrelated to the subject, but duplicating JSON object property names like my_extra_field in your question may be considered not recommended (not sure if vulnerable). See more: Does JSON syntax allow duplicate keys in an object? .
I need to create a json from an Object. The Object contains a List member variable of type Name. Name is a class which contains an enum NameType. below are the class:
public final class Person { private final List<Name> names; }
public class Name {
private final String first;
private final String last;
private final String middle;
private final NameType type;
}
public enum NameType {
X,
Y,
Z
}
The json that is produced for class Person should not include Name for which NameType is Y and Z. Below is the simple way in which I try to generate the json:
Map<String, Object> personMap = mapper.readValue(person!=null ? mapper.writeValueAsString(person) : "{}", typeReference);
I need to remove the key "names" from personMap. I have searched a few ways to remove it before serialization, but that hasn't worked for me. I followed this tutorial: https://www.baeldung.com/jackson-ignore-properties-on-serialization
and tried to remove it using type and using filter, but because this is an enum so didn't work for me. So, I have 2 questions:
Is there a way to not include the Name property as part of serialization based on NameType.
If there is any way to remove after serialization is done.
Below is the structure that is generated for the map.
Thanks in Advance!!
Disclaimer: I never used Jackson before, this was my first contact. I was just looking for a little puzzle to solve while drinking my morning tea, which also enables me to learn something. So I am not sure if there are better or more elegant ways of doing this.
In order to present an MCVE which everyone can easily compile and execute, here are my more complete versions (including getters) of your example classes:
package de.scrum_master.stackoverflow.q71358052;
public enum NameType {
REAL,
ARTIST,
ONLINE
}
package de.scrum_master.stackoverflow.q71358052;
public class Name {
private final String first;
private final String last;
private final String middle;
private final NameType type;
public Name(String first, String last, String middle, NameType type) {
this.first = first;
this.last = last;
this.middle = middle;
this.type = type;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
public String getMiddle() {
return middle;
}
public NameType getType() {
return type;
}
}
package de.scrum_master.stackoverflow.q71358052;
import java.util.List;
public final class Person {
private final List<Name> names;
public Person(List<Name> names) {
this.names = names;
}
public List<Name> getNames() {
return names;
}
}
As described for a similar case in this tutorial, we can use a custom JsonSerializer<Name> in combination with a BeanSerializerModifier, registering them on the ObjectMapper in a SimpleModule:
package de.scrum_master.stackoverflow.q71358052;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class NameSerializer extends JsonSerializer<Name> {
private final JsonSerializer<Object> defaultSerializer;
public NameSerializer(JsonSerializer<Object> serializer) {
defaultSerializer = serializer;
}
#Override
public void serialize(Name value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value.getType() != NameType.REAL)
return;
defaultSerializer.serialize(value, gen, serializers);
}
#Override
public boolean isEmpty(SerializerProvider provider, Name value) {
return value == null || value.getType() != NameType.REAL;
}
}
package de.scrum_master.stackoverflow.q71358052;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import java.util.List;
public class JacksonDemo {
public static void main(String[] args) throws JsonProcessingException {
Person person = new Person(List.of(
new Name("Edward", "Robinson", "G.", NameType.ARTIST),
new Name("Emanuel", "Goldenberg", null, NameType.REAL),
new Name("Эмануэль", "Голденберг", null, NameType.REAL),
new Name("Eddie", "The Gangster", null, NameType.ONLINE)
));
ObjectMapper objectMapper = getObjectMapper();
String personJson = objectMapper.writeValueAsString(person);
System.out.println(personJson.contains("ARTIST")); // false
System.out.println(personJson.contains("ONLINE")); // false
System.out.println(personJson.contains("Goldenberg")); // true
System.out.println(personJson.contains("Голденберг")); // true
System.out.println(personJson);
}
private static ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
objectMapper.registerModule(
new SimpleModule() {
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(
new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (Name.class.isAssignableFrom(beanDesc.getBeanClass()))
return new NameSerializer((JsonSerializer<Object>) serializer);
return serializer;
}
}
);
}
}
);
return objectMapper;
}
}
The console log should be:
false
false
true
true
{"names":[{"first":"Emanuel","last":"Goldenberg","type":"REAL"},{"first":"Эмануэль","last":"Голденберг","type":"REAL"}]}
Oh, by the way and just in case not everybody knows who Edward G. Robinson was...
I'm coding a functionality of handling callbacks from Amazon Simple Email Service via SNS HTTP requests. I would like to parse message provided by Amazon to local object structure. Problem is that SNS is wrapping JSON message into String and it could not be parsed by Jackson. I'm getting an error:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `xxx.email.domain.aws.ses.Notification` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"notificationType":"Delivery","mail":{"timestamp":"2019-10-02T14:43:14.570Z" ... next values of the message ... }}')
Entire message from SNS looks like this one:
{
"Type" : "Notification",
"MessageId" : "4944xxxx-711d-57d4-91b8-8215cxxxxx",
"TopicArn" : "arn:aws:sns:eu-west-1:...",
"Message" : "{\"notificationType\":\"Delivery\",\"mail\":{\"timestamp\":\"2019-10-02T14:43:14.570Z\", ... next values of the message ... },\"delivery\":{\"timestamp\":\"2019-10-02T14:43:16.030Z\", ... next values of the message ... }}",
"Timestamp" : "2019-10-02T14:43:16.062Z",
"SignatureVersion" : "1",
"Signature" : "signature base64",
"SigningCertURL" : "cert url",
"UnsubscribeURL" : "unsubscribe url"
}
My actual local structure looks like this:
#Data
#JsonNaming(PropertyNamingStrategy.UpperCamelCaseStrategy.class)
public class MessageWrapper {
private String type;
private String messageId;
private String topicArn;
private Notification message;
private Date timestamp;
private String signatureVersion;
private String signature;
private String signingCertURL;
private String unsubscribeURL;
}
#Data
public class Notification {
private String notificationType;
private Mail mail;
}
#Data
public class Mail {
private String messageId;
private String source;
private String sourceArn;
private String sourceIp;
private String sendingAccountId;
private String[] destination;
}
I'm looking for some way to tell Jackson that Message should be extracted from a String and treated as a normal JSON.
Edit
deserialization
private MessageWrapper deserializeMessage(String message) throws IOException {
return new ObjectMapper().readValue(message, MessageWrapper.class);
}
I think to solve this you'll need a custom deserializer for Notification field in MessageWrapper class as well as one for the Mail field in the Notification class the like the following:
public class NotificationDeserializer extends JsonDeserializer<Notification> {
#Override
public Notification deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String text = p.getText();
return new ObjectMapper().readValue(text, Notification.class);
}
}
public class MailDeserializer extends JsonDeserializer<Mail> {
#Override
public Mail deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String text = p.getText();
return new ObjectMapper().readValue(text, Mail.class);
}
}
With some annotations on your classes like the following:
#Data
#JsonNaming(PropertyNamingStrategy.UpperCamelCaseStrategy.class)
public class MessageWrapper {
private String type;
private String messageId;
private String topicArn;
#JsonDeserialize(using = NotificationDeserializer.class)
private Notification message;
private Date timestamp;
private String signatureVersion;
private String signature;
private String signingCertURL;
private String unsubscribeURL;
}
#Data
public class Notification {
private String notificationType;
#JsonDeserialize(using = MailDeserializer.class)
private Mail mail;
}
#Data
public class Mail {
private String messageId;
private String source;
private String sourceArn;
private String sourceIp;
private String sendingAccountId;
private String[] destination;
}
EDIT 1
The MailDeserializer isn't actually needed. The NotificationDeserializer alone takes care of the issue.
EDIT 2
Using a new ObjectMapper in the custom deserializer is a must.
message property is of type Notification and Jackson expects JSON Object not string value. In that case you can create custom deserialiser or implement general solution with some kind of loop back implementation. If given payload is not a JSON Object read it as a String and invoke deserialisation again with this String.
To avoid StackOverflowError you need to use another instance of ObjectMapper or use BeanDeserializerModifier to keep BeanDeserializer instance and use it where JSON Object is encountered. Simple example could look like below:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.TextNode;
import lombok.Data;
import lombok.ToString;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.Objects;
import java.util.Set;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
SimpleModule loopBackModule = new SimpleModule();
loopBackModule.setDeserializerModifier(new LoopBackBeanDeserializerModifier(Collections.singleton(Notification.class)));
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(loopBackModule);
MessageWrapper wrapper = mapper.readValue(jsonFile, MessageWrapper.class);
System.out.println(wrapper.getMessage());
}
}
class LoopBackBeanDeserializerModifier extends BeanDeserializerModifier {
private final Set<Class> allowedClasses;
LoopBackBeanDeserializerModifier(Set<Class> allowedClasses) {
this.allowedClasses = Objects.requireNonNull(allowedClasses);
}
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (allowedClasses.contains(beanDesc.getBeanClass())) {
return new LoopBackBeanDeserializer<>((BeanDeserializerBase) deserializer);
}
return deserializer;
}
}
class LoopBackBeanDeserializer<T> extends BeanDeserializer {
private final BeanDeserializerBase baseDeserializer;
protected LoopBackBeanDeserializer(BeanDeserializerBase src) {
super(src);
this.baseDeserializer = src;
}
#Override
public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// if first token is VALUE_STRING we should read it as String and
// run deserialization process again based on this String.
if (p.currentToken() == JsonToken.VALUE_STRING) {
return (T) ((ObjectMapper) p.getCodec()).readValue(p.getText(), _valueClass);
}
// vanilla bean deserialization
return (T) baseDeserializer.deserialize(p, ctxt);
}
}
POJO model is the same. You just need to list classes for which you expect some problems and loop-back mechanism will work for them.
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