Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
143 changes: 143 additions & 0 deletions .github/workflows/jackson3-source-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
name: Jackson 3 source build
on:
pull_request:
workflow_dispatch:
inputs:
source_refs:
description: JSON map of prerequisite repositories to full commit SHAs
type: string
default: '{}'
push:
branches: [master]
permissions:
contents: read
actions: read
env:
# Reviewed master policy and separately qualified candidate; both pins are immutable.
JACKSON3_QUALIFIER_REVISION: f0d2100f972eec765b2b276e7ace38922394f0a6
JACKSON3_CORE_REVISION: 97b2359b92ed3e1d05cac04f2da8760aa954e49d
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
with:
path: consumer
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '25'
- uses: actions/checkout@v4
with:
repository: networknt/light-4j
ref: ${{ env.JACKSON3_QUALIFIER_REVISION }}
path: qualification-policy
- name: Select qualified core and resolve remaining prerequisites
id: prerequisites
env:
GH_TOKEN: ${{ github.token }}
SOURCE_REFS: ${{ inputs.source_refs || '{}' }}
shell: bash
run: |
jq -e --arg core "$JACKSON3_CORE_REVISION" --argjson expected '["http-client","json-overlay","light-4j","light-aws-lambda","light-rest-4j","openapi-parser","yaml-rule"]' '.["light-4j"] == $core and keys == $expected and all(.[]; type == "string" and test("^[0-9a-f]{40}$"))' consumer/ci/jackson3-source-refs.json >/dev/null
source_refs=$(jq -c --argjson overrides "$SOURCE_REFS" '. * $overrides' consumer/ci/jackson3-source-refs.json)
python3 qualification-policy/scripts/resolve_jackson3_consumer_sources.py \
--core-sha "$JACKSON3_CORE_REVISION" \
--repositories '{"json-overlay":null,"openapi-parser":null,"light-rest-4j":null,"light-aws-lambda":null}' --source-refs "$source_refs" \
--report core-qualification.json --github-output "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
with:
repository: networknt/yaml-rule
ref: ${{ steps.prerequisites.outputs.yaml_rule }}
path: sources/yaml-rule
- uses: actions/checkout@v4
with:
repository: networknt/http-client
ref: ${{ steps.prerequisites.outputs.http_client }}
path: sources/http-client
- uses: actions/checkout@v4
with:
repository: networknt/light-4j
ref: ${{ steps.prerequisites.outputs.light_4j }}
path: sources/light-4j
- uses: actions/checkout@v4
with:
repository: networknt/json-overlay
ref: ${{ steps.prerequisites.outputs.json_overlay }}
path: sources/json-overlay
- uses: actions/checkout@v4
with:
repository: networknt/openapi-parser
ref: ${{ steps.prerequisites.outputs.openapi_parser }}
path: sources/openapi-parser
- uses: actions/checkout@v4
with:
repository: networknt/light-aws-lambda
ref: ${{ steps.prerequisites.outputs.light_aws_lambda }}
path: sources/light-aws-lambda
- uses: actions/checkout@v4
with:
repository: networknt/light-rest-4j
ref: ${{ steps.prerequisites.outputs.light_rest_4j }}
path: sources/light-rest-4j
- name: Record source commits
shell: bash
run: |
for source in consumer sources/*; do
echo "$source $(git -C "$source" rev-parse HEAD)"
done | tee source-commits.txt
for source in sources/*; do
printf '{"key":"%s","value":"%s"}\n' "${source##*/}" "$(git -C "$source" rev-parse HEAD)"
done | jq -s from_entries | tee source-refs.json
- name: Build prerequisites and run full tests
shell: bash
env:
GH_TOKEN: ${{ github.token }}
JACKSON3_REUSE_CORE_QUALIFICATION: 1
JACKSON3_CORE_SOURCE: ${{ github.workspace }}/sources/light-4j
JACKSON3_CORE_QUALIFICATION_REPORT: ${{ github.workspace }}/core-qualification.json
JACKSON3_MAVEN_REPO: ${{ runner.temp }}/m2
JACKSON3_YAML_RULE: ${{ github.workspace }}/sources/yaml-rule
JACKSON3_HTTP_CLIENT: ${{ github.workspace }}/sources/http-client
run: |
set -euo pipefail
bash qualification-policy/scripts/verify-jackson3-bootstrap.sh 2>&1 | tee source-build.log
mvn -f sources/yaml-rule/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log
mvn -f sources/json-overlay/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log
mvn -f sources/openapi-parser/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log
mvn -f sources/light-rest-4j/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log
mvn -f sources/light-aws-lambda/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log
mvn -f consumer/pom.xml "-Dmaven.repo.local=$JACKSON3_MAVEN_REPO" clean install 2>&1 | tee -a source-build.log
- name: Test native packaging script
working-directory: consumer
run: python3 tests/test_native_image_script.py
- name: Build and invoke native Runtime locally
working-directory: consumer
env:
MAVEN_REPO_LOCAL: ${{ runner.temp }}/m2
run: |
bash native-image-linux.sh
python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native
- name: Audit resolved artifacts, including releases and Jackson boundaries
env:
SOURCE_MAVEN_REPO: ${{ runner.temp }}/m2
shell: bash
run: |
set -euo pipefail
mvn -B -Dstyle.color=never -f consumer/pom.xml "-Dmaven.repo.local=$SOURCE_MAVEN_REPO" dependency:tree 2>&1 | tee resolved-dependencies.txt
python3 sources/light-4j/scripts/jackson3_artifact_audit.py --repository "$SOURCE_MAVEN_REPO" --tree resolved-dependencies.txt --report artifact-audit.json --require-jackson3-only
- uses: actions/upload-artifact@v4
if: always()
with:
name: source-build-reports
path: |
source-build.log
source-commits.txt
source-refs.json
resolved-dependencies.txt
artifact-audit.json
core-qualification.json
**/target/surefire-reports/**
**/target/*smoke*.log
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,43 @@ software.amazon.awssdk.services.lambda.model.ResourceNotFoundException: Function

### [AuditMiddleware](doc/audit.md)
### [LambdaProxyMiddleware](doc/proxy.md)

## Jackson 3 migration

This source line uses Jackson 3.2.3 and json-schema-validator 3.0.8. Build the Jackson 3 light-4j, http-client, REST and AWS Lambda prerequisites first; older snapshot binaries are not compatible. Jackson annotations retain their com.fasterxml.jackson.annotation package.

The runtime mapper uses Jackson2Defaults with its existing permissive unknown-property override. Mapper instances are immutable. Java time and Optional support are built in. Classic SnakeYAML compatibility pins are retained until the stack-wide cleanup.

Local qualification includes the full JVM suite, sanitized event/validation parity against the Jackson 2 baseline, and an offline GraalVM 25 native-image build. A native-image build does not establish deployed AWS behavior or complete reflection coverage for every middleware/configuration. The separate conformance worktree is deferred.

Native qualification now invokes the executable against a synthetic loopback Lambda Runtime API:

```sh
MAVEN_REPO_LOCAL=/path/to/isolated/m2 bash native-image-linux.sh
python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native
```

The smoke check binds an API Gateway event, runs the local health handler, and verifies success and builder-based error serialization. It uses no AWS endpoint or credentials. `-H:+AddAllCharsets` is required by SnakeYAML Engine's UTF-32 input detection. The check does not qualify every middleware chain or deployed AWS behavior.

### Reproducible Jackson 3 source build

`.github/workflows/jackson3-source-build.yml` builds the prerequisite source stack in an isolated Maven repository, reuses independently qualified full-core test evidence, and runs the prerequisite and full consumer suites. It pins reviewed policy `f0d2100f972eec765b2b276e7ace38922394f0a6` and candidate `97b2359b92ed3e1d05cac04f2da8760aa954e49d`; yaml-rule/http-client are selected from that verified evidence. The complete prerequisite closure is pinned in `ci/jackson3-source-refs.json`, initialized from the successful source-build manifest, with reviewed updates expressed as full SHAs; no default-branch or migration-branch lookup is used. Explicit `source_refs` replay overrides are merged into that map and remain subject to core qualification. The workflow uploads `source-refs.json`, `source-commits.txt`, `core-qualification.json` and Surefire reports. Core registration and workflow-token acceptance are merged in light-4j #2825 and #2826.

The optional `source_refs` workflow-dispatch input replays recorded full SHAs. GitHub enables dispatch only after the workflow exists on the default branch; before merge, reproduce from the recorded source checkouts locally. A green local suite is separate from a successful remote source-build run.

### Exact-source CI qualification

Consumer CI requires retained successful full-reactor evidence for the actual checked-out light-4j SHA and the same yaml-rule/http-client source commits. Core PR CI explicitly checks out the PR head, not its merge ref. Replay dispatches cannot qualify consumers. The attempt-specific evidence artifact records the actual checkout, prerequisite SHAs and test totals. A later pending, failed or cancelled run does not invalidate earlier valid evidence. All API pages and retained attempts are considered; missing or expired evidence blocks reuse. `core-qualification.json` records the accepted run and artifact.

The full dependency ledger is audited across all groups. Every NetworkNT release and snapshot must be source-installed, apart from the explicitly SHA-256-pinned Jackson 3 validator release. Known Jackson 2 yaml-rule and chaos-monkey releases are rejected even with a local-install marker. Jackson 2 dependencies in other groups are reported; ready migrations reject them except shared annotations.


### Updating Jackson 3 sibling source pins after merge

The source manifest deliberately pins reviewed commits, including sibling PR sources. It does not follow branch heads. Later CI-only or documentation commits in a sibling do not require repeatedly chasing its head.

Before merging this dependent PR, merge networknt/light-aws-lambda#168. Resolve each actual merge commit through GitHub (`gh pr view <number> --repo networknt/<repo> --json mergeCommit`); do not substitute the old PR head or a moving branch. For codegen, merge and requalify the Lambda native dependency after AWS first.

Fetch each sibling repository’s master and verify the selected commit is its ancestor with `git merge-base --is-ancestor <merge-sha> origin/master`. Replace the corresponding values in `ci/jackson3-source-refs.json` with those full 40-character merge SHAs in a reviewed dependent change. Keep the remaining closure pins and the qualified core/policy SHAs unchanged. Run fresh source-build CI on that dependent commit and review its resolved-source artifact before merging. A squash or merge commit must be qualified even when its runtime tree matches the earlier pin.

For later sibling updates after this PR merges, use the same manifest-update PR and fresh dependent CI procedure. Never silently advance pins or infer qualification from the sibling’s own green build.
9 changes: 9 additions & 0 deletions ci/jackson3-source-refs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"http-client": "5ecee29dbb08d813512f5379c2e1b902d553c5d4",
"json-overlay": "e309fd63f95317067c9b13a4bed7ac16727bfea5",
"light-4j": "97b2359b92ed3e1d05cac04f2da8760aa954e49d",
"light-aws-lambda": "dddbbc9e7f6f690f8688ceac024728b6a84f4808",
"light-rest-4j": "a31955945036d9108b7e9bb72167415cc9cfc287",
"openapi-parser": "9ae849056e49f7c796fe28bfe6093fbe4847cc5c",
"yaml-rule": "c237cc3a25720f43e57bf571e25d5fa06687aa3b"
}
15 changes: 12 additions & 3 deletions native-image-linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,21 @@ set -euo pipefail

cd -- "$(dirname -- "${BASH_SOURCE[0]}")"

maven_args=()
if [[ -n ${MAVEN_REPO_LOCAL:-} ]]; then
maven_args+=("-Dmaven.repo.local=$MAVEN_REPO_LOCAL")
fi
if [[ ${MAVEN_OFFLINE:-0} == 1 ]]; then
maven_args+=(-o)
fi

# Build the shaded JAR, then obtain its name and Java level from Maven.
mvn clean install
mvn clean install "${maven_args[@]}"
metadata_dir=$(mktemp -d)
trap 'rm -rf -- "$metadata_dir"' EXIT
mvn -q help:evaluate -Dexpression=project.build.finalName \
mvn -q help:evaluate "${maven_args[@]}" -Dexpression=project.build.finalName \
-Doutput="$metadata_dir/final-name"
mvn -q help:evaluate -Dexpression=maven.compiler.target \
mvn -q help:evaluate "${maven_args[@]}" -Dexpression=maven.compiler.target \
-Doutput="$metadata_dir/java-version"
jar_name=$(cat "$metadata_dir/final-name")
java_version=$(cat "$metadata_dir/java-version")
Expand All @@ -24,6 +32,7 @@ docker run --rm --user "$(id -u):$(id -g)" \
"$graalvm_image" \
--enable-url-protocols=http,https \
--no-fallback \
-H:+AddAllCharsets \
--verbose \
--initialize-at-build-time=org.slf4j,ch.qos.logback \
--initialize-at-run-time=io.netty \
Expand Down
12 changes: 6 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.light-4j>2.4.2-SNAPSHOT</version.light-4j>
<version.json-schema-validator>2.0.8</version.json-schema-validator>
<version.jackson>2.22.3</version.jackson>
<version.json-schema-validator>3.0.8</version.json-schema-validator>
<version.jackson>3.2.3</version.jackson>
<version.slf4j>2.0.20</version.slf4j>
<version.encoder>1.5.0</version.encoder>
<version.lambda-core>1.4.0</version.lambda-core>
Expand Down Expand Up @@ -114,7 +114,7 @@
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<groupId>tools.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${version.jackson}</version>
<type>pom</type>
Expand Down Expand Up @@ -356,15 +356,15 @@
<version>${version.lambda-awssdk}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<dependency>
Expand Down
9 changes: 5 additions & 4 deletions src/main/java/com/networknt/aws/lambda/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.ObjectMapper;
import com.networknt.jackson.JacksonDefaults;
import com.networknt.aws.lambda.app.LambdaApp;

import java.io.IOException;


public class Main {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder()
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build();

public static void main(String[] args) throws IOException {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
import tools.jackson.databind.ObjectMapper;
import com.networknt.aws.lambda.handler.MiddlewareHandler;
import com.networknt.aws.lambda.LightLambdaExchange;
import com.networknt.aws.lambda.utility.HeaderKey;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.networknt.aws.lambda.handler.middleware.specification;

import com.fasterxml.jackson.core.JsonProcessingException;
import tools.jackson.core.JacksonException;
import com.networknt.aws.lambda.handler.MiddlewareHandler;
import com.networknt.aws.lambda.LightLambdaExchange;
import com.networknt.config.Config;
Expand Down Expand Up @@ -40,7 +40,7 @@ public OpenApiMiddleware() {
String openapiString = Config.getInstance().getMapper().writeValueAsString(openapi);
LOG.trace("OpenApiMiddleware openapiString: {}", openapiString);
helper = new OpenApiHelper(openapiString);
} catch (JsonProcessingException e) {
} catch (JacksonException e) {
LOG.error("merge specification failed");
throw new RuntimeException("merge specification failed");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.networknt.aws.lambda;

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.lang.reflect.Field;

import static org.junit.jupiter.api.Assertions.*;

class Jackson3NativeCompatibilityTest {
private ObjectMapper mainMapper() throws Exception {
Field field = Main.class.getDeclaredField("OBJECT_MAPPER");
field.setAccessible(true);
return (ObjectMapper) field.get(null);
}

@Test
void runtimeMapperKeepsPermissiveEventBinding() throws Exception {
APIGatewayProxyRequestEvent event = mainMapper().readValue(
"{\"path\":\"/v1/pets\",\"httpMethod\":\"POST\",\"futureField\":true} trailing",
APIGatewayProxyRequestEvent.class);
assertEquals("/v1/pets", event.getPath());
assertEquals("POST", event.getHttpMethod());
}

@Test
void responseRetainsJackson2ShapeAndRoundTrips() throws Exception {
ObjectMapper mapper = mainMapper();
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent()
.withStatusCode(200).withBody("{\"ok\":true}").withIsBase64Encoded(false);
String json = mapper.writeValueAsString(response);
assertEquals("{\"statusCode\":200,\"headers\":null,\"multiValueHeaders\":null,"
+ "\"body\":\"{\\\"ok\\\":true}\",\"isBase64Encoded\":false}", json);
assertEquals(response, mapper.readValue(json, APIGatewayProxyResponseEvent.class));
}

@Test
void runtimeMapperRetainsJackson2StreamDepth() throws Exception {
String nested = "[".repeat(600) + "0" + "]".repeat(600);
assertDoesNotThrow(() -> mainMapper().readTree(nested));
}
}
Loading
Loading