Skip to content

Commit 9c7100d

Browse files
dmealingclaude
andcommitted
fix(java): the generated extract mapper did not compile for most scalar subtypes
Reported as a decimal bug. It was not: 9 of 15 scalar subtypes fail as a single component and 13 of 15 as an array. The reason it looked narrow is that javac reports only the FIRST incompatible argument of a constructor invocation, so a fixture carrying several subtypes surfaces one error and hides the rest. A responding template.prompt types its <Template>Response record through SpringTypeMapper.javaTypeName — BigDecimal, LocalDate, LocalTime, Instant, UUID, URI, InetAddress, Float, and Long for currency. The mapper recognised only Integer/Long/Double/Boolean and fell through to ExtractMap.asString for everything else, with every scalar array going to asStringList. Every one of those pairings is `incompatible types`. It shipped because no test payload in this module carried a decimal, date, time, timestamp, currency, uuid, uri or inet. The record stays strictly typed — ADR-0052 makes <Template>Response a strict record, and a caller who declared a decimal should get a BigDecimal, not a String. So the coercion happens on the way in, in the generated parser, chosen by a function that walks the same instanceof chain javaTypeName does. Helpers are emitted only where used, and every one is never-throws. coerceInet rejects anything that is not an IP literal rather than resolving it: InetAddress .getByName does DNS, and a parser reading untrusted model output must not make a network call. Two further defects surfaced while proving the values, both upstream of codegen: MetaObjectExtractor.assemble violated its own documented contract. The javadoc promises malformed data was classified into the report and assembly cannot throw, but the per-field write goes through DataConverter, which throws on a value it cannot convert — so one unconvertible component destroyed the whole extract. That is the opposite of the lenient tier's purpose: a model answering "31/12/2026" for one date should cost that date, not the twenty fields beside it that parsed. The component is now left unset, which is what "lost" already means elsewhere in that pass. DataConverter.toDate could not parse an ISO-8601 date. The String arm was a bare Long.parseLong, so "2026-03-04" — the form normalization.md puts on the wire — threw NumberFormatException, making every field.date and field.timestamp unassemblable from its own wire representation. Epoch millis is still tried first and only for an all-digit string, so every value that parsed before parses identically; unparseable input still throws, preserving the failure mode for callers outside the lenient tier. Gates: GeneratedScalarExtractLockStepTest compiles generated output for every subtype in FIELD_SUBTYPES in both positions (one payload per subtype, so nothing is masked). GeneratedScalarExtractRoundTripTest asserts VALUES — compiling proves nothing about a coercion that returns null for everything or loses decimal precision through a double — plus the never-throws degradation. Two things deliberately left alone and documented in the round-trip test: a decimal ARRAY cannot populate because DataTypes has no DECIMAL_ARRAY ("arrays-of-decimal aren't in the metamodel", matching C#'s SP-A note), so it compiles and reports the component lost; and DataConverter.toBigDecimal returns BigDecimal.ZERO for an unparseable string rather than null, so garbage reaches a money field as 0. The latter is a core converter the OMDB persistence path also uses, so flipping its contract is its own decision, not a drive-by. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent f332331 commit 9c7100d

6 files changed

Lines changed: 692 additions & 16 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,38 @@ edit (two registered `description` strings) and was ruled a hold, as 1.0.4's was
9898
output now reports the refresh and states what was left untouched. `--refresh-docs` on a
9999
repo that is *not* yet initialized still falls through to a full init and still prints the
100100
scaffold banner, so the branch keys off what actually happened rather than off the flag.
101+
102+
- **Java: the generated extract mapper did not compile for most scalar subtypes.** A
103+
responding `template.prompt` types its `<Template>Response` record through
104+
`SpringTypeMapper.javaTypeName`, which is richly kind-typed (`BigDecimal`, `LocalDate`,
105+
`LocalTime`, `Instant`, `UUID`, `URI`, `InetAddress`, `Float`, `Long` for currency) — while
106+
`SpringOutputParserGenerator`'s mapper recognised only Integer/Long/Double/Boolean and fell
107+
through to `ExtractMap.asString` for everything else, with **every** scalar array going to
108+
`asStringList`. Each mismatch is `incompatible types` at compile time: **9 of 15 scalar
109+
subtypes as a single component, 13 of 15 as an array**. It shipped because no test payload in
110+
the module carried a decimal, date, time, timestamp, currency, uuid, uri or inet — and
111+
because javac reports only the FIRST bad argument of a constructor call, so even a fixture
112+
with several would have looked like one defect (which is exactly how it was first reported,
113+
as a decimal-only bug). The record stays strictly typed per ADR-0052; the generated parser now
114+
coerces on the way in, with never-throws helpers emitted only where used. A new per-subtype
115+
gate compiles generated output for every subtype in both positions.
116+
117+
- **Java: `MetaObjectExtractor.assemble` violated its own documented never-throws contract.**
118+
Its javadoc promises the assembled object comes back with malformed data already classified
119+
into the report and no exception — but the per-field write goes through `DataConverter`, which
120+
throws on a value it cannot convert. One unconvertible component therefore destroyed the
121+
entire extract, the opposite of the lenient tier's purpose: a model answering `"31/12/2026"`
122+
for one date should cost you that date, not the twenty fields beside it that parsed. The
123+
component is now left unset, which is what "lost" already means everywhere else in that pass.
124+
125+
- **Java: `DataConverter.toDate` could not parse an ISO-8601 date.** The `String` arm was a bare
126+
`Long.parseLong`, so it accepted epoch milliseconds and nothing else — a plain `"2026-03-04"`
127+
threw `NumberFormatException`. Since that is the form `normalization.md` puts on the wire, no
128+
`field.date` or `field.timestamp` could be assembled from its own wire representation. It now
129+
accepts epoch millis (tried first, and only for an all-digit string, so every value that
130+
parsed before still parses identically) plus ISO instant, zone-less date-time, and date-only
131+
forms. Unparseable input still throws, preserving the failure mode for callers outside the
132+
lenient tier.
101133
### Added
102134

103135
- **C#: `DbContextGenerator.EmitsReferenceForeignKeys`, an opt-out for the reference-FK

0 commit comments

Comments
 (0)