Jackson deserialization SNS message error MismatchedInputException

半城伤御伤魂 提交于 2019-12-10 16:17:39

问题


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);
}

回答1:


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.




回答2:


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.



来源:https://stackoverflow.com/questions/58204267/jackson-deserialization-sns-message-error-mismatchedinputexception

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!