Goal
Build jdt2jar: a CLI tool + minimal container that compiles JTD schemas into standalone validator JARs at build time, eliminating the need for a JDK 24+ runtime.
User Story
# On any machine with Docker (or natively with JDK 24+)
docker run --rm -v $(pwd)/schemas:/schemas -v $(pwd)/out:/out \
ghcr.io/simbo1905/jdt2jar:latest \
/schemas/user.jtd.json --output /out/user-validator.jar
# The output JAR runs on JDK 21+ with zero ClassFile API dependency
java -jar /out/user-validator.jar --validate /data/payload.json
CLI Interface
jdt2jar <schema.json> [options]
Options:
--output <path> Output JAR path (default: <schema-name>-validator.jar)
--package <name> Java package for generated classes (default: jtd.generated)
--class <name> Validator class name (default: SchemaValidator)
--main Include a main() for standalone CLI validation
--runtime <version> Target bytecode version (default: 21)
--include-sources Also output generated .java files alongside the JAR
--help Show help
Generated JAR Contents
user-validator.jar
├── META-INF/MANIFEST.MF
├── jtd/generated/SchemaValidator.class # implements JtdValidator
├── jtd/generated/SchemaValidator$*.class # helper classes
├── jtd/generated/SchemaValidator.java # (optional, --include-sources)
└── jtd/schema.json # embedded original schema
Usage Patterns
1. Library Integration
// Generated class implements JtdValidator
JtdValidator validator = new jtd.generated.SchemaValidator();
JtdValidationResult result = validator.validate(jsonPayload);
if (!result.isValid()) {
for (var err : result.errors()) {
System.err.println(err.instancePath() + ": " + err.schemaPath());
}
}
2. Standalone CLI Validation
# JAR built with --main flag
java -jar user-validator.jar --validate payload.json
# Exit code 0 = valid, 1 = invalid, 2 = parse error
java -jar user-validator.jar --validate payload.json --format json
# Outputs RFC 8927 error pairs as JSON
3. CI/CD Pipeline
# GitHub Actions example
- name: Compile JTD validators
run: |
docker run --rm -v $PWD/schemas:/schemas -v $PWD/validators:/out \
ghcr.io/simbo1905/jdt2jar:latest \
/schemas/events.jtd.json --output /out/events-validator.jar --main
- name: Validate test fixtures
run: java -jar validators/events-validator.jar --validate tests/fixtures/event.json
4. Maven Plugin (future)
<plugin>
<groupId>io.github.simbo1905.json</groupId>
<artifactId>jdt2jar-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<goals><goal>compile</goal></goals>
<configuration>
<schemaDir>${project.basedir}/schemas</schemaDir>
<outputDir>${project.build.directory}/generated-classes</outputDir>
</configuration>
</execution>
</executions>
</plugin>
Implementation Notes: Container Packaging Specification
Objective
Package the Java CLI tool jdt2jar as:
- a minimal OCI container image
- based on custom
jlink runtime + Distroless Debian 13 ("trixie") base
- suitable for Kubernetes, CI runners, ephemeral execution, rootless runtime
- no shell or package manager in final image
Use: gcr.io/distroless/base-debian13:nonroot
Runtime Requirements
- Java 21 target bytecode
jdt2jar packaged as fat jar or modular jar
- CLI entrypoint:
java -jar /app/jdt2jar.jar
Filesystem Layout
/
├── app/
│ └── jdt2jar.jar
├── jre/
│ ├── bin/java
│ └── lib/...
└── work/
Runtime user: uid=65532, gid=65532 (nonroot distroless default)
Build Strategy
Multi-stage Docker build:
- builder stage: full JDK → compile, tests,
jdeps, jlink
- runtime stage: distroless Debian 13 → stripped runtime + application jar
Java Module Minimization
jdeps --ignore-missing-deps --recursive --multi-release 21 --print-module-deps /build/jdt2jar.jar
Expected minimum modules: java.base
Potential additions: java.logging, java.xml, java.naming, jdk.zipfs
jlink Requirements
jlink --add-modules "$MODULES" --strip-debug --compress=2 --no-header-files --no-man-pages --output /opt/jre
Base Image
FROM gcr.io/distroless/base-debian13:nonroot
Do not use Java distroless variants — runtime is supplied via jlink.
Container Constraints
- No shell: all Docker directives must use JSON/vector form
- TLS: distroless base includes CA certs
- Writable paths: only
/work is writable
- Signal handling: JVM runs as PID 1, must handle SIGTERM/SIGINT
- Logging: stdout/stderr only, no filesystem logging
Recommended JVM Flags
-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-Djava.io.tmpdir=/work/tmp
-XX:+ExitOnOutOfMemoryError (optional)
Dockerfile
# -------------------------------------------------
# Build stage
# -------------------------------------------------
FROM eclipse-temurin:24-jdk AS build
WORKDIR /build
COPY . .
RUN ./mvnw clean package -DskipTests
RUN cp target/jdt2jar.jar /build/jdt2jar.jar
RUN jdeps \
--ignore-missing-deps \
--recursive \
--multi-release 21 \
--print-module-deps \
/build/jdt2jar.jar \
> /build/modules.txt
RUN jlink \
--add-modules $(cat /build/modules.txt) \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages \
--output /opt/jre
# -------------------------------------------------
# Runtime stage
# -------------------------------------------------
FROM gcr.io/distroless/base-debian13:nonroot
WORKDIR /work
COPY --from=build /opt/jre /jre
COPY --from=build /build/jdt2jar.jar /app/jdt2jar.jar
ENTRYPOINT ["/jre/bin/java","-jar","/app/jdt2jar.jar"]
Expected Image Sizes
| Variant |
Size |
| Full JDK image |
350MB–500MB |
| Distroless Java runtime |
120MB–180MB |
| jlink + distroless |
35MB–80MB |
Security Characteristics
Final image contains:
- no package manager, no shell, no compiler, no debugger, no build tools
- attack surface minimized to: glibc + JVM runtime modules + application code
CI Validation
# Verify runtime starts
docker run --rm jdt2jar --help
# Verify no shell exists
docker run --rm jdt2jar /bin/sh
# Expected: executable file not found
# Verify nonroot execution
docker run --rm jdt2jar id
# Expected: uid=65532(nonroot)
Optional Enhancements
- SBOM generation via
syft
- Vulnerability scanning via
grype
- Multi-arch builds (
linux/amd64, linux/arm64) via docker buildx
Goal
Build
jdt2jar: a CLI tool + minimal container that compiles JTD schemas into standalone validator JARs at build time, eliminating the need for a JDK 24+ runtime.User Story
CLI Interface
Generated JAR Contents
Usage Patterns
1. Library Integration
2. Standalone CLI Validation
3. CI/CD Pipeline
4. Maven Plugin (future)
Implementation Notes: Container Packaging Specification
Objective
Package the Java CLI tool
jdt2jaras:jlinkruntime + Distroless Debian 13 ("trixie") baseUse:
gcr.io/distroless/base-debian13:nonrootRuntime Requirements
jdt2jarpackaged as fat jar or modular jarjava -jar /app/jdt2jar.jarFilesystem Layout
Runtime user:
uid=65532, gid=65532(nonroot distroless default)Build Strategy
Multi-stage Docker build:
jdeps,jlinkJava Module Minimization
Expected minimum modules:
java.basePotential additions:
java.logging,java.xml,java.naming,jdk.zipfsjlink Requirements
Base Image
FROM gcr.io/distroless/base-debian13:nonrootDo not use Java distroless variants — runtime is supplied via jlink.
Container Constraints
/workis writableRecommended JVM Flags
Dockerfile
Expected Image Sizes
Security Characteristics
Final image contains:
CI Validation
Optional Enhancements
syftgrypelinux/amd64,linux/arm64) viadocker buildx