8

I am trying to develop an AWS Lambda function that is triggered by events from SQS.

I am using the spring-cloud-function-adapter-aws (version 1.0.0.RELEASE) and in specifically a SpringBootRequestHandler.

However, the ObjectMapper that is being used is case-sensitive and therefore failing to successful convert the Json coming from SQS.

SQS publishes the following Json and it is the Records field in particular that I'm having the problem with.

    {
  "Records": [
    {
      "body": "Hello from SQS!",
      "receiptHandle": "MessageReceiptHandle",
      "md5OfBody": "7b270e59b47ff90a553787216d55d91d",
      "eventSourceARN": "arn:aws:sqs:eu-west-1:123456789012:MyQueue",
      "eventSource": "aws:sqs",
      "awsRegion": "eu-west-1",
      "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
      "attributes": {
        "ApproximateFirstReceiveTimestamp": "1523232000001",
        "SenderId": "123456789012",
        "ApproximateReceiveCount": "1",
        "SentTimestamp": "1523232000000"
      },
      "messageAttributes": {}
    }
  ]
}

I have tried the suggestions in this question, but to no avail. Configuring ObjectMapper in Spring

In my POJO, I've also added the below annotation but it isn't working either whilst it would outside of Lambda.

@JsonProperty("Records")
private List<SqsRecord> Records;

Any help would be much appreciated.

My Lambda handler is defined as:

public class SqsEventHandler extends SpringBootRequestHandler<SqsEvent, String> {}

The POJO defined as:

public class SqsEvent {

@JsonProperty("Records")
private List<SqsRecord> records;

@Data
public class SqsRecord {
    private String body;
    private String receiptHandle;
    private String md5OfBody;
    private String eventSourceARN;
    private String eventSource;
    private String awsRegion;
    private String messageId;
}

}

I expect the Json from the sample message to be able to be read in by the ObjectMapper, but the field "records" is null.

Stephen Gibson
  • 219
  • 2
  • 8

3 Answers3

7

I got this issue solved in a more simple manner.

Referencing https://docs.aws.amazon.com/lambda/latest/dg/java-handler-io-type-stream.html and in specific

if Lambda's serialization approach does not meet your needs, you can use the byte stream implementation

I am now using the SpringBootStreamHandler directly and I have created an ObjectMapper instance with my required configuration options in my Spring Configuration class as:

@Bean
public ObjectMapper objectMapper() {
    final ObjectMapper mapper = new ObjectMapper();
    mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    return mapper;
}
Stephen Gibson
  • 219
  • 2
  • 8
2

we've got this problem with a lot of AWS services. You must define a new mapper like this :

SQSMixin :

 private static interface SQSEventMixin {
    public static final String ATTRIBUTES = "attributes";
    public static final String AWS_REGION = "awsRegion";
    public static final String BODY = "body";
    public static final String EVENT_SOURCE = "eventSource";
    public static final String EVENT_SOURCE_ARN = "eventSourceARN";
    public static final String MD5_OF_BOBY = "md5OfBody";
    public static final String MD5_OF_MESSAGE_ATTRIBUTES = "md5OfMessageAttributes";
    public static final String MESSAGE_ID = "messageId";
    public static final String RECEIPT_HANDLE = "receiptHandle";

    @JsonProperty(value = "Records")
    public List<?> getRecords();

    static interface MessageMixin {
        @JsonProperty(ATTRIBUTES)
        public String getAttributes();

        @JsonProperty(ATTRIBUTES)
        public void setAttributes(String attributes);

        @JsonProperty(AWS_REGION)
        public String getAwsRegion();

        @JsonProperty(AWS_REGION)
        public void setAwsRegion(String awsRegion);

        @JsonProperty(BODY)
        public Object getBody();

        @JsonProperty(BODY)
        public void setBody(Object body);

        @JsonProperty(EVENT_SOURCE)
        public String getEventSource();

        @JsonProperty(EVENT_SOURCE)
        public void setEventSource(String eventSource);

        @JsonProperty(EVENT_SOURCE_ARN)
        public String getEventSourceArn();

        @JsonProperty(EVENT_SOURCE_ARN)
        public void setEventSourceArn(String eventSourceArn);

        @JsonProperty(MD5_OF_BOBY)
        public String getMd5OfBody();

        @JsonProperty(MD5_OF_BOBY)
        public void setMd5OfBody(String md5OfBody);

        @JsonProperty(MD5_OF_MESSAGE_ATTRIBUTES)
        public String getMd5OfMessageAttributes();

        @JsonProperty(MD5_OF_MESSAGE_ATTRIBUTES)
        public void setMd5OfMessageAttributes(String md5OfMessageAttributes);

        @JsonProperty(MESSAGE_ID)
        public String getMessageId();

        @JsonProperty(MESSAGE_ID)
        public void setMessageId(String messageId);

        @JsonProperty(RECEIPT_HANDLE)
        public String getReceiptHandle();

        @JsonProperty(RECEIPT_HANDLE)
        public void setReceiptHandle(String receiptHandle);
    }
}

A Strategy for record :

private static class UpperCaseRecordsPropertyNamingStrategy extends PropertyNamingStrategy.PropertyNamingStrategyBase {

    private static final long serialVersionUID = 1L;

    @Override
    public String translate(String propertyName) {
        if (propertyName.equals("records")) {
            return "Records";
        }
        return propertyName;
    }
}

Formatter for Date :

private static final DateTimeFormatter dateTimeFormatter = ISODateTimeFormat.dateTime()
        .withZone(new FixedDateTimeZone("GMT", "GMT", 0, 0));

private static class DateTimeMapperModule extends SimpleModule {

    private static final long serialVersionUID = 1L;

    public DateTimeMapperModule() {
        super("DateTimeMapperModule");

        super.addSerializer(DateTime.class, new DateTimeSerializer());
        super.addDeserializer(DateTime.class, new DateTimeDeserializer());
    }
}

private static class DateTimeSerializer extends JsonSerializer<DateTime> {

    @Override
    public void serialize(DateTime value, JsonGenerator gen, SerializerProvider provider) throws IOException {

        gen.writeString(dateTimeFormatter.print(value));
    }
}

private static class DateTimeDeserializer extends JsonDeserializer<DateTime> {

    @Override
    public DateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException {

        return dateTimeFormatter.parseDateTime(parser.getText());
    }
}

And declare your mapper :

    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
    mapper.setPropertyNamingStrategy(new UpperCaseRecordsPropertyNamingStrategy());
    mapper.registerModule(new DateTimeMapperModule());
    mapper.addMixIn(SQSMessage.class, SQSEventMixin.MessageMixin.class);
    SQSEvent request = mapper.convertValue(inputObject, SQSEvent.class);
CiliaFred
  • 21
  • 1
0

There is already an official library that is supporting this: https://aws.amazon.com/blogs/opensource/testing-aws-lambda-functions-written-in-java/

<dependency>
  <groupId>com.amazonaws</groupId>
  <artifactId>aws-lambda-java-tests</artifactId>
  <version>1.0.0</version>
  <scope>test</scope>
</dependency>

Also have surefire in your plugins:

<build>
  <plugins>
    <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.22.2</version>
    </plugin>
  </plugins>
</build>

Example:

SQSEvent input = EventLoader.loadEvent("/sqsEvent.json", SQSEvent.class);