Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0ffb292
test: 🧪 add roundtrip serialization test utility
Mar 22, 2026
fc09501
test: fix false positives epoch format errors, added comment about this
Mar 23, 2026
a210699
test: fixed false positves DateTime differences
Mar 23, 2026
5e9d7b8
test: fixing error in lex event fixture
Mar 23, 2026
269a7f2
test: fixing connect event
Mar 24, 2026
ac8a420
test: fixing api gateway proxy event false negative
Mar 24, 2026
d8e33a1
test: fixing cloudfront and s3 event false negatives
Mar 24, 2026
f2c250c
chore: adding mise to gitignore
Mar 24, 2026
bb52901
test: fix MSKFirehose, LexEvent, RabbitMQ, APIGatewayV2Auth and Activ…
Mar 24, 2026
8c4052e
test: Add round-trip fixtures for 4 registered events
Mar 24, 2026
f031481
test: Add round-trip fixtures for 21 unregistered events
Mar 24, 2026
b5a16c4
test: Add round-trip tests for 11 response event types
Mar 25, 2026
7770e90
chore: fixed comment
Mar 25, 2026
2769a8b
test: including IAM Policy Reponse roundtrip test
Mar 25, 2026
27a1641
test: add test for JsonNodeUtils
Mar 25, 2026
8764367
chore: remove unwanted file on origin
Mar 25, 2026
0697d14
docs: add tests 1.1.2 changelog entry
Mar 26, 2026
bb000e3
chore: remove entry in changelog
Mar 27, 2026
322da3d
fix: 🔧 updating again jacjson 2.15.4 -> 2.18.6
Mar 25, 2026
1709917
fix: fixing an error in github actions that was causing false positiv…
Mar 26, 2026
b41ee3f
docs: update changelogs
Mar 26, 2026
97f7cb0
chore: releasing aws-lambda-tests
Mar 27, 2026
783e5d2
chore: add space
Mar 27, 2026
61b375e
Merge branch 'main' into dmelfi/jackson-2.18.6-upgrade
darklight3it Mar 30, 2026
b92aad6
Merge branch 'main' into dmelfi/jackson-2.18.6-upgrade
darklight3it Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/aws-lambda-java-serialization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:

# Package and install target module
- name: Package serialization with Maven
run: mvn -B package install --file aws-lambda-java-serialization/pom.xml
run: mvn -B install --file aws-lambda-java-serialization/pom.xml

# Run tests
- name: Run tests from aws-lambda-java-tests
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ experimental/aws-lambda-java-profiler/integration_tests/helloworld/bin
.vscode
.kiro
build
mise.toml
7 changes: 7 additions & 0 deletions aws-lambda-java-serialization/RELEASE.CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
### March 26, 2026
`1.4.0`:
- Update `jackson-databind` dependency from 2.15.4 to 2.18.6
- Replace deprecated `PropertyNamingStrategy.PascalCaseStrategy` with `PropertyNamingStrategies.UpperCamelCaseStrategy`
- The regression reported in 1.3.1 was a false positive caused by a CI workflow bug (`mvn package install` running the shade plugin twice, corrupting the artifact). Fixed by using `mvn install` instead.

### March 19, 2026
`1.3.1`:
- Revert `jackson-databind` dependency from 2.18.6 to 2.15.4
- Revert `PropertyNamingStrategies.UpperCamelCaseStrategy` to `PropertyNamingStrategy.PascalCaseStrategy`
- Note: reverted due to a suspected regression in Joda DateTime deserialization; later confirmed to be a false positive (see 1.4.0)

### March 11, 2026
`1.3.0`:
Expand Down
2 changes: 0 additions & 2 deletions aws-lambda-java-serialization/mise.toml

This file was deleted.

4 changes: 2 additions & 2 deletions aws-lambda-java-serialization/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-serialization</artifactId>
<version>1.3.1</version>
<version>1.4.0</version>
<packaging>jar</packaging>

<name>AWS Lambda Java Runtime Serialization</name>
Expand Down Expand Up @@ -32,7 +32,7 @@
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<relocation.prefix>com.amazonaws.lambda.thirdparty</relocation.prefix>
<jackson.version>2.15.4</jackson.version>
<jackson.version>2.18.6</jackson.version>
<gson.version>2.10.1</gson.version>
<json.version>20231013</json.version>
<owasp.version>7.3.2</owasp.version>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,17 @@
import com.fasterxml.jackson.databind.module.SimpleModule;

/**
* The AWS API represents a date as a double, which specifies the fractional
* number of seconds since the epoch. Java's Date, however, represents a date as
* a long, which specifies the number of milliseconds since the epoch. This
* class is used to translate between these two formats.
* The AWS API represents a date as a double (fractional seconds since epoch).
* Java's Date uses a long (milliseconds since epoch). This module translates
* between the two formats.
*
* <p>
* <b>Round-trip caveats:</b> The serializer always writes via
* {@link JsonGenerator#writeNumber(double)}, so integer epochs
* (e.g. {@code 1428537600}) round-trip as decimal ({@code 1.4285376E9}).
* Sub-millisecond precision is lost because {@link java.util.Date}
* has milliseconds precision.
* </p>
*
* This class is copied from LambdaEventBridgeservice
* com.amazon.aws.lambda.stream.ddb.DateModule
Expand Down
6 changes: 6 additions & 0 deletions aws-lambda-java-tests/RELEASE.CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
### March 27, 2026
`1.1.3`:
- Add serialization round-trip tests covering 66 event classes
- Bumped `aws-lambda-java-serialization` to version `1.4.0` (Jackson `2.15.x` → `2.18.6`)
- Bumped `aws-lambda-java-events` to version `3.16.1`

### August 26, 2021
`1.1.1`:
- Bumped `aws-lambda-java-events` to version `3.11.0`
Expand Down
14 changes: 9 additions & 5 deletions aws-lambda-java-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-tests</artifactId>
<version>1.1.2</version>
<version>1.1.3</version>
<packaging>jar</packaging>

<name>AWS Lambda Java Tests</name>
Expand Down Expand Up @@ -40,18 +40,22 @@
-->
<junit.version>5.9.2</junit.version>
<jacoco.maven.plugin.version>0.8.7</jacoco.maven.plugin.version>
<aws-lambda-java-serialization.version>1.4.0</aws-lambda-java-serialization.version>
<aws-lambda-java-events.version>3.16.1</aws-lambda-java-events.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<assertj-core.version>3.27.7</assertj-core.version>
</properties>

<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-serialization</artifactId>
<version>1.2.0</version>
<version>${aws-lambda-java-serialization.version}</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-events</artifactId>
<version>3.16.1</version>
<version>${aws-lambda-java-events.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand All @@ -71,13 +75,13 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
<version>${commons-lang3.version}</version>
</dependency>

<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.amazonaws.services.lambda.runtime.tests;

import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.JsonNode;
import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Iterator;
import java.util.List;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.joda.time.DateTime;

/**
* Utility methods for working with shaded Jackson {@link JsonNode} trees.
*
* <p>
* Package-private — not part of the public API.
* </p>
*/
class JsonNodeUtils {

private static final Pattern ISO_DATE_REGEX = Pattern.compile("\\d{4}-\\d{2}-\\d{2}T.+");

private JsonNodeUtils() {
}

/**
* Recursively removes all fields whose value is {@code null} from the
* tree. This mirrors the serializer's {@code Include.NON_NULL} behaviour
* so that explicit nulls in the fixture don't cause false-positive diffs.
*/
static JsonNode stripNulls(JsonNode node) {
if (node.isObject()) {
ObjectNode obj = (ObjectNode) node;
Iterator<String> fieldNames = obj.fieldNames();
while (fieldNames.hasNext()) {
String field = fieldNames.next();
if (obj.get(field).isNull()) {
fieldNames.remove();
} else {
stripNulls(obj.get(field));
}
}
} else if (node.isArray()) {
for (JsonNode element : node) {
stripNulls(element);
}
}
return node;
}

/**
* Recursively walks both trees and collects human-readable diff lines.
*/
static void diffNodes(String path, JsonNode expected, JsonNode actual, List<String> diffs) {
if (expected.equals(actual))
return;

// Compares two datetime strings by parsed instant, because DateTimeModule
// normalizes the format on serialization (e.g. "+0000" → "Z", "Z" → ".000Z")
if (areSameDateTime(expected.textValue(), actual.textValue())) {
return;
}

if (expected.isObject() && actual.isObject()) {
TreeSet<String> allKeys = new TreeSet<>();
expected.fieldNames().forEachRemaining(allKeys::add);
actual.fieldNames().forEachRemaining(allKeys::add);
for (String key : allKeys) {
diffChild(path + "." + key, expected.get(key), actual.get(key), diffs);
}
} else if (expected.isArray() && actual.isArray()) {
for (int i = 0; i < Math.max(expected.size(), actual.size()); i++) {
diffChild(path + "[" + i + "]", expected.get(i), actual.get(i), diffs);
}
} else {
diffs.add("CHANGED " + path + " : " + summarize(expected) + " -> " + summarize(actual));
}
}

/**
* Compares two strings by parsed instant when both look like ISO-8601 dates,
* because DateTimeModule normalizes format on serialization
* (e.g. "+0000" → "Z", "Z" → ".000Z").
*/
private static boolean areSameDateTime(String expected, String actual) {
if (expected == null || actual == null
|| !ISO_DATE_REGEX.matcher(expected).matches()
|| !ISO_DATE_REGEX.matcher(actual).matches()) {
return false;
}
return DateTime.parse(expected).equals(DateTime.parse(actual));
}

private static void diffChild(String path, JsonNode expected, JsonNode actual, List<String> diffs) {
if (expected == null)
diffs.add("ADDED " + path + " = " + summarize(actual));
else if (actual == null)
diffs.add("MISSING " + path + " (was " + summarize(expected) + ")");
else
diffNodes(path, expected, actual, diffs);
}

private static String summarize(JsonNode node) {
if (node == null) {
return "<absent>";
}
String text = node.toString();
return text.length() > 80 ? text.substring(0, 77) + "..." : text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.amazonaws.services.lambda.runtime.tests;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer;
import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers;
import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.JsonNode;
import com.amazonaws.lambda.thirdparty.com.fasterxml.jackson.databind.ObjectMapper;

import java.util.ArrayList;
import java.util.List;

/**
* Framework-agnostic assertion utilities for verifying Lambda event
* serialization.
*
* <p>
* When opentest4j is on the classpath (e.g. JUnit 5.x / JUnit Platform),
* assertion failures are reported as
* {@code org.opentest4j.AssertionFailedError}
* which enables rich diff support in IDEs. Otherwise, falls back to plain
* {@link AssertionError}.
* </p>
*
*/
public class LambdaEventAssert {

private static final ObjectMapper MAPPER = new ObjectMapper();

/**
* Round-trip using the registered {@link LambdaEventSerializers} path
* (Jackson + mixins + DateModule + DateTimeModule + naming strategies).
*
* <p>
* The check performs two consecutive round-trips
* (JSON &rarr; POJO &rarr; JSON &rarr; POJO &rarr; JSON) and compares the
* original JSON tree against the final output tree. A single structural
* comparison catches both:
* </p>
* <ul>
* <li>Fields silently dropped during deserialization</li>
* <li>Non-idempotent serialization (output changes across round-trips)</li>
* </ul>
*
* @param fileName classpath resource name (must end with {@code .json})
* @param targetClass the event class to deserialize into
* @throws AssertionError if the original and final JSON trees differ
*/
public static <T> void assertSerializationRoundTrip(String fileName, Class<T> targetClass) {
PojoSerializer<T> serializer = LambdaEventSerializers.serializerFor(targetClass,
ClassLoader.getSystemClassLoader());

if (!fileName.endsWith(".json")) {
throw new IllegalArgumentException("File " + fileName + " must have json extension");
}

byte[] originalBytes;
try (InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(fileName)) {
if (stream == null) {
throw new IllegalArgumentException("Could not load resource '" + fileName + "' from classpath");
}
originalBytes = toBytes(stream);
} catch (IOException e) {
throw new UncheckedIOException("Failed to read resource " + fileName, e);
}

// Two round-trips: original → POJO → JSON → POJO → JSON
// We are doing 2 passes so we can check instability problems
// like UnstablePojo in LambdaEventAssertTest
ByteArrayOutputStream firstOutput = roundTrip(new ByteArrayInputStream(originalBytes), serializer);
ByteArrayOutputStream secondOutput = roundTrip(
new ByteArrayInputStream(firstOutput.toByteArray()), serializer);

// Compare original tree against final tree.
// Strip explicit nulls from the original because the serializer is
// configured with Include.NON_NULL — null fields are intentionally
// omitted and that is not a data-loss bug.
try {
JsonNode originalTree = JsonNodeUtils.stripNulls(MAPPER.readTree(originalBytes));
JsonNode finalTree = MAPPER.readTree(secondOutput.toByteArray());

if (!originalTree.equals(finalTree)) {
List<String> diffs = new ArrayList<>();
JsonNodeUtils.diffNodes("", originalTree, finalTree, diffs);

if (!diffs.isEmpty()) {
StringBuilder msg = new StringBuilder();
msg.append("Serialization round-trip failure for ")
.append(targetClass.getSimpleName())
.append(" (").append(diffs.size()).append(" difference(s)):\n");
for (String diff : diffs) {
msg.append(" ").append(diff).append('\n');
}

String expected = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(originalTree);
String actual = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(finalTree);
throw buildAssertionError(msg.toString(), expected, actual);
}
}
} catch (IOException e) {
throw new UncheckedIOException("Failed to parse JSON for tree comparison", e);
}
}

private static <T> ByteArrayOutputStream roundTrip(InputStream stream, PojoSerializer<T> serializer) {
T event = serializer.fromJson(stream);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
serializer.toJson(event, outputStream);
return outputStream;
}

private static byte[] toBytes(InputStream stream) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] chunk = new byte[4096];
int n;
while ((n = stream.read(chunk)) != -1) {
buffer.write(chunk, 0, n);
}
return buffer.toByteArray();
}

/**
* Tries to create an opentest4j AssertionFailedError for rich IDE diff
* support. Falls back to plain AssertionError if opentest4j is not on
* the classpath.
*/
private static AssertionError buildAssertionError(String message, String expected, String actual) {
try {
// opentest4j is provided by JUnit Platform (5.x) and enables
// IDE diff viewers to show expected vs actual side-by-side.
Class<?> cls = Class.forName("org.opentest4j.AssertionFailedError");
return (AssertionError) cls
.getConstructor(String.class, Object.class, Object.class)
.newInstance(message, expected, actual);
} catch (ReflectiveOperationException e) {
return new AssertionError(message + "\nExpected:\n" + expected + "\nActual:\n" + actual);
}
}
}
Loading