This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A single-purpose Java library that parses and builds devcontainer.json files. Published to Maven Central as wtf.metio.devcontainer:devcontainer.java. No CLI, no runtime — it is a model + Jackson glue.
JDK 25 (global.jdkVersion in pom.xml), Maven. Inherits most config from the wtf.metio.maven:maven-parent POM. That parent does not manage third-party dependency versions, so this POM pins them itself: Jackson and JUnit via imported BOMs in <dependencyManagement>, record-builder via the version.record-builder property (shared between the dependency and the annotation-processor path — they must move together).
- Build & test:
mvn --batch-mode verify - Native-compatibility check (what CI's PR check runs — the GraalVM
nativeprofile):mvn --batch-mode --activate-profiles=native verify - Single test class:
mvn test -Dtest=DevcontainerParsingTest
verify also runs the quality gates (see below). .github/workflows/verify.yml runs the native command above on GraalVM 25, so PMD, SpotBugs, and the native smoke check all gate every PR.
The local machine has no JDK/Maven installed; run builds through ilo (see global instructions) or in the GraalVM container CI uses.
- PMD (
check+cpd-check, from the parent) with a focused ruleset inconfig/pmd/ruleset.xml. The generated-sources root is excluded viaexcludeRoots. - SpotBugs (
check) withconfig/spotbugs/exclude.xml. The exclusions are deliberate: representation-exposure findings are expected for records holdingList/Map, and the generated*Builderclasses are not ours to restyle.
Both fail the build on findings. The PMD engine is pinned to 7.x so it can parse JDK 25 sources. When a quality gate flags generated code, exclude the generated artifact (root or class pattern) rather than the rule globally.
The schema is modeled as a graph of Java records, rooted at Devcontainer. Supporting records (Build, Command, HostRequirements, PortAttribute) and enums (OnAutoForward, Protocol, ShutdownAction, UserEnvProbe, WaitFor) cover the nested and constrained fields. Records carry the spec docs as Javadoc on each component.
- record-builder annotation processor (
io.soabase.record-builder,providedscope) generates a<Record>Builderfor every@RecordBuilderrecord at compile time. Records implement<Record>Builder.Withto get wither methods. The build method is renamed tocreate()via@RecordBuilder.Options. These*Builderclasses do not exist until you compile — don't go looking for them in source. - Jackson 3 (
tools.jackson.*packages,tools.jacksonBOM coordinates — not the Jackson 2com.fasterxml.jackson.*). The mapper is immutable and built viaJsonMapper.builder()...build(); there are noenable()/disable()mutators on an instance. Jackson 3 throws uncheckedJacksonException, so theparse(...)methods declare no checkedIOException. - Parsing lives entirely in
Devcontainer.parse(...)overloads (Path/File/String), backed bydefaultObjectMapper(): aJsonMapperthat disablesFAIL_ON_UNKNOWN_PROPERTIES(forward-compat with spec additions) and enablesACCEPT_SINGLE_VALUE_AS_ARRAY(the spec lets array fields appear as a single scalar). Commandis a polymorphic field. In the spec a lifecycle command may be a string, a string array, or an object of named commands.Commandholds all three (string,array,object) with at most one non-null, populated byCommandDeserializer(aStdDeserializerwired via@JsonDeserialize). Theobjectvariant recurses into moreCommands.- JPMS:
module-info.javaopens the package totools.jackson.databind(needed for reflective deserialization) and requires record-builder + java.compiler asstatic(compile-only).
The library's contract is that downstream consumers (e.g. ilo) can native-compile an app that depends on it. src/main/resources/META-INF/native-image/.../reflect-config.json registers every record and enum for reflection so Jackson works in a native image. When you add a new record or enum type to the model, add a corresponding entry to this file, or native-image deserialization will fail at runtime (not compile time) in consumers.
The native profile verifies this contract by mimicking a consumer: it adds src/native/java as a source root (so the stand-in never ships in the published jar), builds NativeImageSmokeTest into a native image via the plugin's compile-no-fork goal, then runs it (exec). Building exercises reflect-config.json; running surfaces missing runtime reflection metadata. When you add a model field worth covering, exercise it in that smoke main. It deliberately does not native-compile the JUnit test suite — that would only test JUnit's own native compatibility, not the library's.
DevcontainerParsingTest is a @TestFactory that pairs JSON fixtures in src/test/resources/*.json with assertion lambdas. To test a new field or parsing case: add a fixture file and a Map.entry("fixture.json", devcontainer -> ...) with the expected values. DevcontainerBuilderTest covers the generated builders/withers.
- Every file carries an SPDX header; the project license is 0BSD. Match the existing header when adding files.
- DCO sign-off is required: commit with
git commit --signoff. - Releases are calendar-versioned and automated (
.github/workflows/release.yml, Friday cron, version =date +'%Y.%-m.%-d'). A release only happens when commits since the last tag touchedsrc/main/javaorpom.xml. The POM version stays0.0.0-SNAPSHOTin the tree; the real version is injected at release time.