docs(adr-051): YAML/JSON parity contract for the OAI-12 normalization pipeline
§5 records the post-OAI-12 from_yaml contract: duplicate keys rejected loudly on YAML (with the empirical correction that serde_json 1.0.151's Value path last-wins rather than errors — the YAML side is the stricter one), non-finite floats rejected with JSON-pointer context, merge keys applied via apply_merge (shallow, referencing keys win; the one deliberate YAML 1.2 deviation), scalar-key stringification matching the core schema, and the no-new-bounds note (the walk stays inside yaml_serde's parse-time limits). Verification: cargo doc --no-deps clean; module-doc cross-check in openapi_spec.rs matches this contract.
This commit is contained in:
@@ -133,6 +133,79 @@ default-features model avoids. The dependency is small (a pure-Rust YAML
|
||||
parser, no native code), consistent with the existing default-features
|
||||
philosophy of the crate.
|
||||
|
||||
### 5. YAML/JSON parity contract (review 002 OAI-12)
|
||||
|
||||
> **Amendment (2026-08-30, review 002 OAI-12):** the original delivery fed
|
||||
> `yaml_serde::from_str::<serde_json::Value>` straight into `from_value`
|
||||
> with zero post-parse normalization, and three verified corruptions
|
||||
> flowed through unimpeded: duplicate keys silently last-won; `.inf`/
|
||||
> `.nan` scalars silently became `null` (`serde_json::Number::from_f64`
|
||||
> is `None` for non-finite floats); merge keys (`<<: *anchor`) survived
|
||||
> as literal `<<` properties. `from_yaml` now runs a normalization
|
||||
> pipeline; this section records the resulting contract. The same
|
||||
> document may still mean different things through `from_json` and
|
||||
> `from_yaml` only where explicitly stated below — every difference is
|
||||
> loud on the YAML side.
|
||||
|
||||
The `from_yaml` pipeline is: an explicit parse into `yaml_serde::Value`
|
||||
→ [`apply_merge()`](https://docs.rs/yaml_serde) → one structural
|
||||
normalization pass into `serde_json::Value` → the shared `from_value`
|
||||
path. Per-construct rules:
|
||||
|
||||
1. **Duplicate mapping keys are rejected loudly on the YAML path** — the
|
||||
explicit `yaml_serde::Value` parse rejects them natively with the key
|
||||
and its line/column, naming the document position. The JSON path
|
||||
*silently last-wins* (`serde_json::Value`'s `visit_map` inserts into
|
||||
a map; verified empirically against serde_json 1.0.151 — the
|
||||
oft-assumed "JSON errors on duplicates" behavior does not hold for
|
||||
the `Value` target). The YAML path is deliberately the stricter one:
|
||||
a duplicate-key document fails YAML import instead of silently
|
||||
meaning different things through the two entry points. Callers who
|
||||
need JSON-path tolerance of duplicates have it by construction; the
|
||||
asymmetry is documented and tested, not hidden.
|
||||
2. **Non-finite floats are rejected loudly.** YAML 1.2 core schema has
|
||||
`.inf`/`-.inf`/`.INF`/`.nan` (and case variants) as floats;
|
||||
`serde_json::Number` cannot represent them, and an unguarded
|
||||
conversion would advertise `null` where the document declares e.g.
|
||||
`maximum: .inf` — silently dropping a declared constraint from the
|
||||
schema. The normalization pass fails import with the offending
|
||||
value's JSON pointer and the rendered value.
|
||||
3. **Merge keys are applied, not advertised.** `<<: *anchor` resolves
|
||||
per the YAML merge type before conversion; `<<` never survives as a
|
||||
literal property in the parsed spec. The semantics are `yaml_serde`'s
|
||||
`apply_merge()`: shallow `entry().or_insert()` into the referencing
|
||||
mapping — the referencing mapping's explicit keys win, and a `<<`
|
||||
value that is neither a mapping nor a sequence of mappings fails
|
||||
loudly. This is the one deliberate, tested deviation from strict
|
||||
YAML 1.2 core-schema processing (§2): merge keys are a YAML 1.1
|
||||
mechanism that YAML tooling and authors universally expect to work,
|
||||
and the alternative (rejecting `<<` loudly) would refuse
|
||||
anchor-heavy real-world documents that JSON cannot even express. A
|
||||
producer wanting strict 1.2 processing can avoid `<<` in favor of
|
||||
explicit repetition.
|
||||
4. **Non-string mapping keys** are stringified on both paths by
|
||||
construction (JSON keys are strings; YAML scalar keys are rendered
|
||||
exactly as the YAML 1.2 core schema renders the scalar: `200:` →
|
||||
`"200"` matches JSON `{"200": ...}`, `true:` → `"true"`, `1.5:` →
|
||||
`"1.5"`). Keys with no round-trip-stable string form — nullish keys
|
||||
(`~`, empty) and collection keys (sequences, mappings) — are
|
||||
rejected loudly with the key's JSON pointer instead of being
|
||||
stringified through YAML's debug rendering.
|
||||
5. **Scalar interpretation is identical by dependency version.** Both
|
||||
paths go through the YAML 1.2 core schema or JSON's stricter grammar
|
||||
(§2 as amended): bare `yes`/`no`/`on`/`off` are strings, `!!str 200`
|
||||
is the string `"200"`, and unknown tags fail loudly on both paths.
|
||||
The JSON-first rule of §2 remains the defensive lock against a
|
||||
future YAML-parser swap changing this.
|
||||
|
||||
The normalization pass adds no new resource bounds: it walks structure
|
||||
already accepted by `yaml_serde`'s parse-time limits (recursion limit,
|
||||
alias jump limit, repetition limit — re-verified during review 002), is
|
||||
linear in document size, and allocates only the converted document
|
||||
plus error messages. Fixtures for the normalization are all linear or
|
||||
bounded; the `$ref` resolver's budgets (OAI-01/OAI-11) are downstream
|
||||
and unchanged.
|
||||
|
||||
### 4. Scope boundary: `to_openapi` output is not affected
|
||||
|
||||
`to_openapi` generates the published gateway doc, served at `GET
|
||||
|
||||
Reference in New Issue
Block a user