From 96dd98c0c00c5b2dc233a8a0fd64cfbf3fa260fa Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 22:50:13 -0400 Subject: [PATCH 1/3] Share the permissive Lambda runtime mapper --- .../aws/lambda/LambdaRuntimeMappers.java | 18 ++++++++++++++++++ .../com/networknt/aws/lambda/Runtime.java | 5 +---- .../Jackson3RuntimeCompatibilityTest.java | 19 ++++++++++--------- 3 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 custom-runtime/src/main/java/com/networknt/aws/lambda/LambdaRuntimeMappers.java diff --git a/custom-runtime/src/main/java/com/networknt/aws/lambda/LambdaRuntimeMappers.java b/custom-runtime/src/main/java/com/networknt/aws/lambda/LambdaRuntimeMappers.java new file mode 100644 index 0000000..401abd6 --- /dev/null +++ b/custom-runtime/src/main/java/com/networknt/aws/lambda/LambdaRuntimeMappers.java @@ -0,0 +1,18 @@ +package com.networknt.aws.lambda; + +import com.networknt.jackson.JacksonDefaults; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.ObjectMapper; + +/** Shared immutable event mapper for the JVM and native Lambda runtimes. */ +public final class LambdaRuntimeMappers { + private static final ObjectMapper MAPPER = JacksonDefaults.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); + + private LambdaRuntimeMappers() { } + + /** Keeps the framework defaults while accepting future AWS event properties. */ + public static ObjectMapper mapper() { + return MAPPER; + } +} diff --git a/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java b/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java index 0b99f2d..3d7713b 100644 --- a/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java +++ b/custom-runtime/src/main/java/com/networknt/aws/lambda/Runtime.java @@ -3,7 +3,6 @@ import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.networknt.jackson.JacksonDefaults; import com.networknt.utility.NioUtils; import java.io.IOException; import java.lang.reflect.Method; @@ -11,14 +10,12 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.net.HttpURLConnection; -import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; public class Runtime { private static final String REQUEST_ID_HEADER = "lambda-runtime-aws-request-id"; - private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); + private static final ObjectMapper OBJECT_MAPPER = LambdaRuntimeMappers.mapper(); public static void main(String[] args) throws IOException { while(true) { diff --git a/custom-runtime/src/test/java/com/networknt/aws/lambda/Jackson3RuntimeCompatibilityTest.java b/custom-runtime/src/test/java/com/networknt/aws/lambda/Jackson3RuntimeCompatibilityTest.java index 387d202..1134d10 100644 --- a/custom-runtime/src/test/java/com/networknt/aws/lambda/Jackson3RuntimeCompatibilityTest.java +++ b/custom-runtime/src/test/java/com/networknt/aws/lambda/Jackson3RuntimeCompatibilityTest.java @@ -3,7 +3,6 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import org.junit.jupiter.api.Test; -import tools.jackson.databind.ObjectMapper; import java.util.List; import java.util.Map; @@ -14,15 +13,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue; class Jackson3RuntimeCompatibilityTest { - private ObjectMapper runtimeMapper() throws Exception { - var field = Runtime.class.getDeclaredField("OBJECT_MAPPER"); - field.setAccessible(true); - return (ObjectMapper) field.get(null); + @Test + void reusesOneImmutableMapperWithoutChangingFrameworkDefaults() { + org.junit.jupiter.api.Assertions.assertSame(LambdaRuntimeMappers.mapper(), LambdaRuntimeMappers.mapper()); + assertFalse(LambdaRuntimeMappers.mapper().isEnabled(tools.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)); + assertTrue(com.networknt.jackson.JacksonDefaults.mapper().isEnabled(tools.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)); } + @Test void bindsAwsGatewayEventsWithoutJackson2Types() throws Exception { - var mapper = runtimeMapper(); + var mapper = LambdaRuntimeMappers.mapper(); var request = mapper.readValue("{\"path\":\"/pets\",\"httpMethod\":\"POST\"," + "\"body\":\"雪\",\"isBase64Encoded\":true,\"requestContext\":{\"requestId\":\"fixture\"}," + "\"multiValueHeaders\":{\"X-Test\":[\"one\",\"two\"]}}", APIGatewayProxyRequestEvent.class); @@ -40,12 +41,12 @@ void preservesTheMeasuredResponseWireShape() throws Exception { .withMultiValueHeaders(Map.of("Set-Cookie", List.of("a=1", "b=2"))); assertEquals("{\"statusCode\":201,\"headers\":{\"Content-Type\":\"application/json\"}," + "\"multiValueHeaders\":{\"Set-Cookie\":[\"a=1\",\"b=2\"]},\"body\":\"雪\\nbody\"," - + "\"isBase64Encoded\":true}", runtimeMapper().writeValueAsString(response)); + + "\"isBase64Encoded\":true}", LambdaRuntimeMappers.mapper().writeValueAsString(response)); } @Test void retainsBuilderBindingAndNullPrimitiveHandling() throws Exception { - var mapper = runtimeMapper(); + var mapper = LambdaRuntimeMappers.mapper(); var response = mapper.readValue("{\"code\":null,\"message\":\"fixture error\"}", DefaultResponse.class); assertEquals(0, response.getCode()); assertEquals("{\"code\":0,\"message\":\"fixture error\"}", mapper.writeValueAsString(response)); @@ -56,7 +57,7 @@ void retainsBuilderBindingAndNullPrimitiveHandling() throws Exception { @Test void retainsPermissiveRuntimeBinding() throws Exception { - var mapper = runtimeMapper(); + var mapper = LambdaRuntimeMappers.mapper(); var request = mapper.readValue("{\"futureField\":1,\"isBase64Encoded\":false} {}", APIGatewayProxyRequestEvent.class); assertNull(request.getPath()); assertFalse(request.getIsBase64Encoded()); From 9d59c6a74112a15f702cfd76ceb401680ad05518 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Mon, 26 Oct 2026 22:50:14 -0400 Subject: [PATCH 2/3] Isolate caller-owned schemas during Lambda validation --- .../aws/lambda/validator/SchemaValidator.java | 8 +- .../lambda/validator/SchemaIsolationTest.java | 80 +++++++++++++++++++ 2 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaIsolationTest.java diff --git a/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java b/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java index 52ed649..5725d9d 100644 --- a/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java +++ b/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java @@ -75,7 +75,8 @@ public SchemaValidator() { * for use in references. */ public SchemaValidator(final OpenApi3 api) { - this.jsonNode = api == null ? null : Overlay.toJson((OpenApi3Impl)api).get(COMPONENTS_FIELD); + JsonNode components = api == null ? null : Overlay.toJson((OpenApi3Impl)api).get(COMPONENTS_FIELD); + this.jsonNode = components == null ? null : components.deepCopy(); this.pathType = PathType.JSON_POINTER; } @@ -114,10 +115,11 @@ private Status doValidate(final JsonNode value, final JsonNode schema, boolean t List processingReport = null; try { + JsonNode validationSchema = schema.deepCopy(); if(jsonNode != null) { - ((ObjectNode)schema).set(COMPONENTS_FIELD, jsonNode); + ((ObjectNode)validationSchema).set(COMPONENTS_FIELD, jsonNode.deepCopy()); } - Schema jsonSchema = getRegistry(typeLoose, nullableKeywordEnabled, validationPathType).getSchema(schema); + Schema jsonSchema = getRegistry(typeLoose, nullableKeywordEnabled, validationPathType).getSchema(validationSchema); processingReport = jsonSchema.validate(value); } catch (Exception e) { LOG.error("Unable to validate the value against the OpenAPI schema", e); diff --git a/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaIsolationTest.java b/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaIsolationTest.java new file mode 100644 index 0000000..5a041b7 --- /dev/null +++ b/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaIsolationTest.java @@ -0,0 +1,80 @@ +package com.networknt.aws.lambda.validator; + +import com.networknt.jackson.JacksonDefaults; +import com.networknt.oas.OpenApiParser; +import com.networknt.oas.model.OpenApi3; +import org.junit.jupiter.api.Test; +import tools.jackson.databind.JsonNode; + +import java.util.ArrayList; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.*; + +class SchemaIsolationTest { + private static OpenApi3 api(String type) throws Exception { + return (OpenApi3) new OpenApiParser().parse(""" + {"openapi":"3.0.3","info":{"title":"isolation","version":"1"}, + "paths":{},"components":{"schemas":{"Value":{"type":"%s"}}}} + """.formatted(type), java.nio.file.Path.of("schema-isolation.json").toUri().toURL(), false); + } + + private static JsonNode reference() { + return JacksonDefaults.mapper().readTree("{\"$ref\":\"#/components/schemas/Value\"}"); + } + + @Test + void validationNeverAddsComponentsToTheCallerSchema() throws Exception { + var validator = new SchemaValidator(api("integer")); + var schema = reference(); + var original = schema.deepCopy(); + assertNull(validator.validate(JacksonDefaults.mapper().readTree("7"), schema, false, false)); + assertNotNull(validator.validate(JacksonDefaults.mapper().readTree("\"invalid\""), schema, false, false)); + assertEquals(original, schema); + assertFalse(schema.has("components")); + } + + @Test + void callerOwnedComponentsRemainUnchanged() throws Exception { + var schema = JacksonDefaults.mapper().readTree(""" + {"$ref":"#/components/schemas/Value", + "components":{"schemas":{"Value":{"type":"string"}}}} + """); + var original = schema.deepCopy(); + var validator = new SchemaValidator(api("integer")); + // API components still take precedence for this validation, as before. + assertNull(validator.validate(JacksonDefaults.mapper().readTree("7"), schema, false, false)); + assertEquals(original, schema); + } + + @Test + void concurrentValidatorsKeepTheirOwnComponentsAndProfiles() throws Exception { + var integerValidator = new SchemaValidator(api("integer")); + var stringValidator = new SchemaValidator(api("string")); + var schema = reference(); + var original = schema.deepCopy(); + var number = JacksonDefaults.mapper().readTree("7"); + var text = JacksonDefaults.mapper().readTree("\"7\""); + var tasks = new ArrayList>(); + for (int i = 0; i < 40; i++) { + tasks.add(() -> { + assertNull(integerValidator.validate(number, schema, false, false)); + assertNotNull(stringValidator.validate(number, schema, false, false)); + assertNull(stringValidator.validate(text, schema, false, false)); + assertNotNull(integerValidator.validate(text, schema, false, false)); + assertNull(integerValidator.validate(text, schema, true, false)); + return null; + }); + } + var executor = Executors.newFixedThreadPool(4); + try { + for (var future : executor.invokeAll(tasks, 10, TimeUnit.SECONDS)) future.get(1, TimeUnit.SECONDS); + } finally { + executor.shutdownNow(); + assertTrue(executor.awaitTermination(1, TimeUnit.SECONDS)); + } + assertEquals(original, schema); + } +} From 4aee8bfd2b062196b20c6e0e29bd4ac0786a717b Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Tue, 27 Oct 2026 09:07:29 -0400 Subject: [PATCH 3/3] Document schema snapshot semantics and clarify isolation tests --- .../aws/lambda/validator/SchemaValidator.java | 2 ++ .../aws/lambda/validator/SchemaIsolationTest.java | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java b/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java index 5725d9d..36791a0 100644 --- a/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java +++ b/schema-validator/src/main/java/com/networknt/aws/lambda/validator/SchemaValidator.java @@ -70,6 +70,8 @@ public SchemaValidator() { /** * Build a new validator for the given API specification. + * Components are snapshotted at construction. Later changes to the API overlay are not observed; + * construct a new validator after changing the specification. * * @param api The API to build the validator for. If provided, is used to retrieve schemas in components * for use in references. diff --git a/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaIsolationTest.java b/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaIsolationTest.java index 5a041b7..9906723 100644 --- a/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaIsolationTest.java +++ b/schema-validator/src/test/java/com/networknt/aws/lambda/validator/SchemaIsolationTest.java @@ -6,19 +6,26 @@ import org.junit.jupiter.api.Test; import tools.jackson.databind.JsonNode; +import java.nio.file.Path; import java.util.ArrayList; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class SchemaIsolationTest { private static OpenApi3 api(String type) throws Exception { + // Resolution base for the inline document; the parser does not read this file. + var documentUrl = Path.of("schema-isolation.json").toUri().toURL(); return (OpenApi3) new OpenApiParser().parse(""" {"openapi":"3.0.3","info":{"title":"isolation","version":"1"}, "paths":{},"components":{"schemas":{"Value":{"type":"%s"}}}} - """.formatted(type), java.nio.file.Path.of("schema-isolation.json").toUri().toURL(), false); + """.formatted(type), documentUrl, false); } private static JsonNode reference() { @@ -50,7 +57,7 @@ void callerOwnedComponentsRemainUnchanged() throws Exception { } @Test - void concurrentValidatorsKeepTheirOwnComponentsAndProfiles() throws Exception { + void concurrentValidationKeepsCallerInputsIsolated() throws Exception { var integerValidator = new SchemaValidator(api("integer")); var stringValidator = new SchemaValidator(api("string")); var schema = reference();