Skip to content

fix(intent): an unknown key is an error, never a silent drop (#6541) - #6748

Merged
delchev merged 1 commit into
masterfrom
fix/intent-unknown-keys
Aug 15, 2026
Merged

fix(intent): an unknown key is an error, never a silent drop (#6541)#6748
delchev merged 1 commit into
masterfrom
fix/intent-unknown-keys

Conversation

@delchev

@delchev delchev commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Closes #6541.

The failure

The typed mapping is Gson, which ignores unknown properties. A key the model does not declare was therefore accepted and discarded — parse 200, generate 200, code generation 201, publish 200, and the promise the author wrote simply absent at runtime.

The seed-row half (the case that opened the issue) is worse: the emitted CSV loses the column, and when that column is a NOT NULL FK, CSVIM then skips every row. Observed end-to-end cost: a rates nomenclature imported as zero rows, downstream computations running on empty data, every pipeline step green. The trigger was a pure case slip — contributionScheme: for the relation ContributionScheme.

The fix

Two halves, one rule — an unknown key is a validation error naming the key, where it sits, and the nearest declared name.

  • UnknownKeyValidator walks the RAW YAML tree against the model classes — declared fields, honouring @SerializedName (extends) — so the known-key set can never drift from what the parser actually maps. It generalizes IntentParser.rejectRemovedNumberKeys, which special-cased three keys for exactly this reason (that check stays: its migration messages beat "unknown key").
  • IntentParser.validateSeedRowKeys matches a seed row's keys against the entity's own field and to-one relation names, plus the stage marker — exactly what CsvimIntentGenerator emits columns for. A collection relation (no FK column) is rejected too.

A pure case difference is called out explicitly ((names are case-sensitive)), being the slip hardest to see by eye. The issues join the structural ones, so one parse still reports everything to fix:

unknown key [requird] at [entities[Rate].relations[Scheme]] - did you mean [required]?
seed [rates] row references [scheme] which is not a field or a to-one relation of [Rate] - did you mean [Scheme]? (names are case-sensitive)

Scope, deliberately

The walk follows typed structure only. A Map-valued property carries keys drawn from the model being described, not from the intent schema — a map: / defaults: projection, a relation's where:, a widget's at: — and stays opaque here; those are the per-feature validators' vocabulary.

One member of that family is a real remaining gap: a process step's args: keys are kind-dispatched (assignee / form / if / then / else / timeout / expire / setField / delegate / …) and are still silently dropped when misspelled. Closing it means an authored per-kind key set rather than a reflected one, which is a separate change with its own false-positive risk; filed as #6749.

Verification

  • Red-first: 9 of the 12 new unit cases fail with the two call sites commented out (the other 3 are the "still parses" guards).
  • No false positives: swept the parser over 63 real production intents plus 259 in-repo fixtures (every inline YAML text block in the test and IT sources, and the six .intent test resources) — zero unknown-key hits, and a deliberate canary was caught.
  • mvn test -pl components/engine/engine-intent: 557 tests green.
  • IntentEngineIT (HTTP, smoke set): 47 tests green, including the new parse_rejects_an_unknown_key_and_an_unknown_seed_row_key asserting the exact 422 messages — the outermost layer this change is observable at.
  • formatter:validate clean; javadoc clean under -P release.

Docs

🤖 Generated with Claude Code

The typed mapping is Gson, which ignores unknown properties, so a key the
model does not declare was accepted and discarded: parse 200, generate 200,
code generation 201, publish 200, and the promise the author wrote simply
absent at runtime. The seed-row half was worse - the CSV lost the column, and
when that column was a NOT NULL FK the import skipped EVERY row, so a rates
nomenclature imported as zero rows and downstream computations ran on empty
data, with every pipeline step green.

Two halves, one rule:

- `UnknownKeyValidator` walks the RAW YAML tree against the model classes -
  declared fields, honouring `@SerializedName` - so the known-key set can
  never drift from what the parser actually maps. It follows typed structure
  only: a Map-valued property (a step's `args`, a `map:`/`defaults:`
  projection, a relation's `where:`, a widget's `at:`) carries names from the
  model being described, not from the intent schema, and stays opaque here.
- `validateSeedRowKeys` matches a seed row's keys against the entity's own
  field and to-one relation names plus the `stage` marker - exactly what
  `CsvimIntentGenerator` emits columns for.

Both messages name the key, where it sits and the nearest declared name, and
say "(names are case-sensitive)" on a pure case slip - the case that opened
the issue (`contributionScheme` for the relation `ContributionScheme`). The
issues join the structural ones, so one parse still reports everything.

Generalizes `rejectRemovedNumberKeys`, which special-cased three keys for
exactly this reason; it stays, because its migration messages beat "unknown
key".

Verified red-first (9 of 12 unit cases fail without it) and against 63 real
production intents plus every in-repo fixture - no false positives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

intent: unknown seed row key is silently dropped - should be a generation error

1 participant