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
32 changes: 31 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,40 @@ jobs:
for k in totals: totals[k]+=int(r.get(k,'0'))
except Exception:
pass
exp_tests=1354
exp_tests=1355
exp_skipped=0
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")
sys.exit(1)
print(f"OK totals: {totals}")
PY

docker-build:
name: Build Docker image (Java 25)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: oracle
java-version: '25'
cache: 'maven'

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: .
file: jdt2jar/Dockerfile
push: false
load: true
tags: jdt2jar:ci

- name: Smoke test
run: |
docker run --rm jdt2jar:ci --help
33 changes: 32 additions & 1 deletion .github/workflows/maven.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,35 @@ jobs:
restore-keys: ${{ runner.os }}-m2-java25

- name: Test full project (Java 25)
run: mvn clean test
run: mvn clean test

docker-build:
name: Build Docker image (Java 25)
runs-on: ubuntu-latest
needs: test-java25

steps:
- uses: actions/checkout@v4

- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'oracle'
cache: 'maven'

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: .
file: jdt2jar/Dockerfile
push: false
load: true
tags: jdt2jar:ci

- name: Smoke test
run: |
docker run --rm jdt2jar:ci --help
57 changes: 56 additions & 1 deletion .github/workflows/release-on-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ on:
- 'release/[0-9]*.[0-9]*.[0-9]*'

permissions:
contents: write # push tags, push commits
contents: write
pull-requests: write
packages: write

concurrency:
group: release-${{ github.ref }}
Expand All @@ -34,11 +35,26 @@ jobs:
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}

- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"

- name: Create GitHub Release with notes
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
generate_release_notes: true
body: |
## Docker Image

Pre-built distroless container image available on GitHub Container Registry:

```bash
docker pull ghcr.io/${{ github.repository_owner }}/java.util.json.java21/jdt2jar:${{ steps.version.outputs.version }}
docker pull ghcr.io/${{ github.repository_owner }}/java.util.json.java21/jdt2jar:latest
```

See [jdt2jar/README.md](https://github.com/${{ github.repository }}/blob/main/jdt2jar/README.md) for usage.

- name: Build and Deploy to Central (release profile)
env:
Expand Down Expand Up @@ -76,3 +92,42 @@ jobs:
--base main \
--head "${{ steps.prbranch.outputs.branch }}" \
|| echo "PR already exists or nothing to compare"

docker-publish:
name: Publish Docker image to GHCR
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: oracle
java-version: '25'
cache: 'maven'

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract version from tag
id: version
run: echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: jdt2jar/Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository_owner }}/java.util.json.java21/jdt2jar:${{ steps.version.outputs.version }}
ghcr.io/${{ github.repository_owner }}/java.util.json.java21/jdt2jar:latest
46 changes: 16 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ This repo is organized into the following modules:
| Module | What it is | JDK |
| --- | --- | --- |
| `json-java21` | Core `java.util.json` backport (parser, immutable types, `Json` API) | 21+ |
| `json-java21-jtd` | JSON Type Definition (JTD) validator implementing RFC 8927, with stack-machine interpreter and optional bytecode codegen interface (`JtdValidator`) | 21+ |
| `json-java21-jtd-codegen` | Bytecode code generator for JTD schemas using JDK 24+ ClassFile API (JEP 484); generates Java 21-compatible `.class` files for ~9x faster validation | 24+ (auto-skipped on JDK 21) |
| `json-java21-jtd` | JTD (RFC 8927) stack-machine interpreter — ideal for infrequent config parsing and one-time validation | 21+ |
| `json-java21-jtd-codegen` | Bytecode code generator for JTD schemas — ahead-of-time compiled validators for repeated hot-path validation | 24+ (auto-skipped on JDK 21) |
| `jdt2jar` | CLI + distroless container to pre-compile JTD schemas into standalone validator JARs (eliminates JDK 24+ runtime requirement) | 24+ (auto-skipped on JDK 21) |
| `json-java21-jsonpath` | JsonPath query engine over `jdk.sandbox.java.util.json` values (Goessner-style: filters, slices, recursive descent, unions) | 21+ |
| `json-compatibility-suite` | JSON Test Suite conformance reporter (tests against [nst/JSONTestSuite](https://github.com/nst/JSONTestSuite)) | 21+ |
| `json-java21-api-tracker` | Daily upstream API drift detector — fetches OpenJDK sandbox sources, compares public API signatures, reports differences | 25+ |
Expand Down Expand Up @@ -335,9 +336,12 @@ Such vulnerabilities existed at one point in the upstream OpenJDK sandbox implem

## JSON Type Definition (JTD) Validator

This repo contains an incubating JTD validator that has the core JSON API as its only dependency. This sub-project demonstrates how to build realistic JSON heavy logic using the API. It follows Data Oriented Programming principles: it compiles JTD schemas into an immutable structure of records. For validation it parses the JSON document to the generic structure and uses the thread-safe parsed schema and a stack to visit and validate the parsed JSON.
This repo includes two JTD validation paths for different use cases:

A complete JSON Type Definition validator is included (module: json-java21-jtd).
- **Interpreter** ([`json-java21-jtd`](json-java21-jtd/README.md)) — stack-machine validator for infrequent config parsing and one-time validation. Runs on JDK 21+ with zero extra dependencies.
- **Bytecode codegen** ([`json-java21-jtd-codegen`](json-java21-jtd-codegen/README.md)) — generates dedicated validator classes for repeated hot-path validation (~9x faster). Requires JDK 24+ at build time; generated classes run on JDK 21+.

> `java.util.json` has entered the JDK incubator (`jdk.incubator.json`). Once the API stabilises in the JDK itself, generated bytecode validators can depend directly on future JDK classes with zero library overhead.

### Empty Schema `{}` Semantics (RFC 8927)

Expand All @@ -349,43 +353,19 @@ Per **RFC 8927 (JSON Typedef)**, the empty schema `{}` is the **empty form** and
> `empty = {}`
> **Empty form:** A schema in the empty form accepts all JSON values and produces no errors.

⚠️ Note: Some tools or in-house validators mistakenly interpret `{}` as "object with no
properties allowed." **That is not JTD.** This implementation follows RFC 8927 strictly.

```java
import json.java21.jtd.Jtd;
import jdk.sandbox.java.util.json.*;

// Compile JTD schema
JsonValue schema = Json.parse("""
{
"properties": {
"name": {"type": "string"},
"age": {"type": "int32"}
}
}
""");

// Validate JSON
JsonValue data = Json.parse("{\"name\":\"Alice\",\"age\":30}");
JsonValue schema = Json.parse("{\"properties\":{\"name\":{\"type\":\"string\"}}}");
JsonValue data = Json.parse("{\"name\":\"Alice\"}");
Jtd validator = new Jtd();
Jtd.Result result = validator.validate(schema, data);
// result.isValid() => true
```

### JTD RFC 8927 Compliance

The validator provides full RFC 8927 compliance with comprehensive test coverage:

```bash
# Run all JTD compliance tests
./mvnw test -pl json-java21-jtd -Dtest=JtdSpecIT

# Run with detailed logging
./mvnw test -pl json-java21-jtd -Djava.util.logging.ConsoleHandler.level=FINE
```

Features:
- ✅ Eight mutually-exclusive schema forms (RFC 8927 §2.2)
- ✅ Standardized error format with instance and schema paths
- ✅ Primitive type validation with proper ranges
Expand All @@ -394,6 +374,12 @@ Features:
- ✅ Discriminator tag exemption from additional properties
- ✅ Stack-based validation preventing StackOverflowError

## JTD to JAR Compiler (Optional)

An optional `jdt2jar` CLI tool and distroless Docker image are available for pre-compiling JTD schemas into standalone validator JARs at build time. This eliminates the JDK 24+ runtime requirement for generated validators — the JARs run on JDK 21+.

See [`jdt2jar/README.md`](jdt2jar/README.md) for build instructions, container usage, and the pre-built image on GitHub Container Registry (`ghcr.io`).

## Building

Requires JDK 21 or later. Build with Maven:
Expand Down
21 changes: 21 additions & 0 deletions jdt2jar/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Build outputs
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move Docker ignore rules to active build context

This ignore file is never applied by the current CI/release Docker builds because they invoke docker build with context: . (repository root), and Docker only reads .dockerignore from the context root (or a Dockerfile.dockerignore companion). As a result, large/sensitive files (for example .git/, .env, and local artifacts) are still sent in build context despite being listed here, which increases build time and can leak unintended content into remote builders.

Useful? React with 👍 / 👎.

**/target/

# IDE
.idea/
*.iml
.vscode/
.eclipse/

# OS
.DS_Store

# Git
.git/
.gitignore

# Env
.env

# Local test artifacts
/tmp/jdt2jar-*/
16 changes: 16 additions & 0 deletions jdt2jar/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# syntax=docker/dockerfile:1

FROM eclipse-temurin:24-jdk AS build
WORKDIR /build
COPY . .
RUN ["./mvnw", "-pl", "jdt2jar", "-am", "package", "-DskipTests", "-Dsurefire.failIfNoSpecifiedTests=false"]
RUN ["java", "-cp", "/build/jdt2jar/target/jdt2jar.jar", "json.java21.jdt2jar.build.DockerImageBuilder", "/build/jdt2jar/target/jdt2jar.jar", "/opt/jre"]
RUN ["mkdir", "-p", "/empty-work/tmp", "/empty-app"]

FROM gcr.io/distroless/base-debian13:nonroot
COPY --from=build --chown=65532:65532 /empty-work /work
COPY --from=build --chown=65532:65532 /empty-app /app
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -Djava.io.tmpdir=/work/tmp -XX:+ExitOnOutOfMemoryError"
COPY --from=build /opt/jre /jre
COPY --from=build /build/jdt2jar/target/jdt2jar.jar /app/jdt2jar.jar
ENTRYPOINT ["/jre/bin/java","-jar","/app/jdt2jar.jar"]
90 changes: 90 additions & 0 deletions jdt2jar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# jdt2jar

`jdt2jar` compiles a JTD schema into a standalone validator JAR at build time. The generated JAR runs on JDK 21+ with no JDK 24+ runtime dependency.

## Use Case

This tool bridges the gap between the interpreter and codegen paths:

- **Interpreter** ([`json-java21-jtd`](../json-java21-jtd/README.md)): ideal for infrequent config parsing — simple, no build step, runs on JDK 21+.
- **Codegen** ([`json-java21-jtd-codegen`](../json-java21-jtd-codegen/README.md)): ideal for repeated hot-path validation — ~9x faster, but requires JDK 24+ at runtime.
- **jdt2jar**: pre-compiles schemas into validator JARs at build time (using JDK 24+), then deploys them to any JDK 21+ runtime. Best for CI/CD pipelines, distroless containers, or environments where you want JIT-optimised validators without shipping a JDK 24+ runtime.

> **Future note**: `java.util.json` has entered the JDK incubator (`jdk.incubator.json`). Once the API stabilises in the JDK itself, generated bytecode validators can depend directly on future JDK classes rather than this backport, making them even more efficient with zero library overhead.

## CLI

```bash
jdt2jar <schema.json> [options]
```

Options:

- `--output <path>`: output JAR path (default: `<schema-name>-validator.jar`)
- `--package <name>`: generated package name (default: `jtd.generated`)
- `--class <name>`: validator class name (default: `SchemaValidator`)
- `--main`: include a standalone `java -jar` entry point
- `--runtime <version>`: target bytecode version (default: 21)
- `--include-sources`: write a companion `.java` file next to the JAR
- `--help`: show usage

## Container Image

A minimal distroless container image is available for offline schema compilation without a full JDK.

### Pre-built Image (GitHub Container Registry)

```bash
# Pull the latest image
docker pull ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest

# Pull a specific release version
docker pull ghcr.io/simbo1905/java.util.json.java21/jdt2jar:2026.02.05
```

### Build Locally

Requires Docker and JDK 24+ (for the build stage). Build from the repository root:

```bash
docker build -t jdt2jar -f jdt2jar/Dockerfile .
```

### Usage

```bash
# Show help
docker run --rm ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest --help

# Compile a schema to a validator JAR (using docker cp for file I/O)
cid=$(docker create --name jdt2jar-build ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest /work/person.jtd.json --output /work/person-validator.jar --main)
docker cp person.jtd.json jdt2jar-build:/work/person.jtd.json
docker start -a jdt2jar-build
docker cp jdt2jar-build:/work/person-validator.jar .
docker rm jdt2jar-build

# Validate a payload with the generated JAR
java -jar person-validator.jar --validate payload.json
# Or validate inside a container
cid=$(docker create --name jdt2jar-validate --entrypoint /jre/bin/java ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest -jar /work/person-validator.jar --validate /work/payload.json)
docker cp person-validator.jar jdt2jar-validate:/work/person-validator.jar
docker cp payload.json jdt2jar-validate:/work/payload.json
docker start -a jdt2jar-validate
docker rm jdt2jar-validate
```

### Image Properties

- **Base**: `gcr.io/distroless/base-debian13:nonroot`
- **Runtime**: jlink-minimized JDK 24 (~40 MB)
- **Total size**: ~111 MB disk / ~31 MB content
- **User**: `nonroot` (uid 65532)
- **Shell**: none (distroless)
- **Writable directories**: `/work` (for schema input and JAR output)

### Security Scanning

```bash
syft packages image:ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest
grype ghcr.io/simbo1905/java.util.json.java21/jdt2jar:latest
```
Loading
Loading