Ошибка сообщения SNS десериализации Джексона MismatchedInputException

Я кодирую функцию обработки обратных вызовов из Amazon Simple Email Service через HTTP-запросы SNS. Я хотел бы проанализировать сообщение, предоставленное Amazon, в структуре локальных объектов. Проблема в том, что SNS упаковывает сообщение JSON в строку, и Джексон не может его проанализировать. Я получаю сообщение об ошибке:

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 ... }}')

Все сообщение из соцсети выглядит так:

 {
  "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"
}

Моя реальная локальная структура выглядит так:

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

Я ищу способ сообщить Джексону, что Message следует извлекать из String и обрабатывать как обычный JSON.

Изменить

десериализация

private MessageWrapper deserializeMessage(String message) throws IOException {
    return new ObjectMapper().readValue(message, MessageWrapper.class);
}

person MilyGosc    schedule 02.10.2019    source источник
comment
Разве вам не нужно добавлять одну и ту же аннотацию @JsonNaming(PropertyNamingStrategy.UpperCamelCaseStrategy.class) ко всем трем вашим классам?   -  person Ivan    schedule 02.10.2019
comment
@Ivan, даже если я добавлю эту аннотацию ко всем из них, я все равно получаю MismatchedInputException   -  person MilyGosc    schedule 02.10.2019
comment
Не могли бы вы добавить фрагмент кода, который анализируется через Джексона?   -  person mcarlin    schedule 02.10.2019
comment
@mcarlin добавил в конце   -  person MilyGosc    schedule 02.10.2019


Ответы (2)


Я думаю, чтобы решить эту проблему, вам понадобится специальный десериализатор для поля Notification в классе MessageWrapper, а также один для поля Mail в классе Notification, как показано ниже:

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

С некоторыми аннотациями к вашим классам, например:

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

ИЗМЕНИТЬ 1

MailDeserializer на самом деле не нужен. Только NotificationDeserializer решает эту проблему.

ИЗМЕНИТЬ 2

Использование нового ObjectMapper в пользовательском десериализаторе является обязательным.

person mcarlin    schedule 02.10.2019
comment
Спасибо за вашу помощь :) NotificationDeserializer достаточно для решения моей проблемы, MailDeserializer не нужен, потому что значение Message является правильным JSON (добавление этого вызовет даже исключение). - person MilyGosc; 02.10.2019
comment
@mcarlin, чтобы использовать root ObjectMapper, вы можете использовать ((ObjectMapper) p.getCodec()), но в этом примере это не сработает. Он выдаст StackOverflowError, потому что ObjectMapper все время хотел бы использовать нестандартную реализацию. Итак, создание нового ObjectMapper обязательно. - person Michał Ziober; 02.10.2019

message имеет тип Notification, а Jackson ожидает JSON Object, а не string value. В этом случае вы можете создать собственный десериализатор или реализовать общее решение с какой-либо реализацией обратной петли. Если заданная полезная нагрузка не JSON Object, прочтите ее как String и снова вызовите десериализацию с этим String.

Чтобы избежать StackOverflowError, вам нужно использовать другой экземпляр ObjectMapper или BeanDeserializerModifier, чтобы сохранить экземпляр BeanDeserializer и использовать его там, где встречается JSON Object. Простой пример может выглядеть так:

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 модель такая же. Вам просто нужно перечислить классы, для которых вы ожидаете некоторых проблем, и механизм loop-back будет работать для них.

person Michał Ziober    schedule 02.10.2019