docs(arch): amend ADR-051 §2 — yaml_serde 0.10.x is YAML 1.2, not 1.1

Implementation verified yaml_serde 0.10.4 (and serde_yaml 0.9) implement the
YAML 1.2 core schema, where bare yes/no/on/off are strings, not booleans. The
original §2 rationale cited YAML 1.1 boolean-coercion as a present hazard
justifying JSON-first detection; it does not exist with this dependency
version.

The JSON-first decision is retained (Accepted ADR) — reframed as a defensive
default that locks the contract against a future YAML-parser swap, not a
guard against a present hazard. §2 heading, Consequences, and Assumption #2
amended; an in-place amendment note records what changed and when. The two
coercion-related tests already document yaml_serde 0.10.x's actual behavior
with explicit comments. Source doc-comments on from_yaml/from_str corrected
to stop propagating the flawed assumption. 256 tests pass; clippy clean.
This commit is contained in:
glm-5.2 committed 2026-07-06 13:30:59 +00:00
1 parent 21c4f7d252
commit 14c8340380
4 files changed
+97 -52

No files matched your search

+1 -1
View File
@@ -54,7 +54,7 @@ protocol), and hosts the HTTP-backed call-protocol adapters
| [047](../../decisions/047-remove-direct-call-http-surface.md) | Remove the Direct-Call HTTP Surface; Gateway Is the Sole Invoke Path | `POST /{service}/{op}` direct-call surface removed; the 5 gateway endpoints are the sole invoke path; per-caller `AccessControl`-filtered `/search` is the discovery; ADR-036's non-routing clauses survive |
| [048](../../decisions/048-websocket-native-session-not-gateway.md) | WebSocket Carries the Native Call-Protocol Session, Not the Gateway Shape | WS is the native `EventEnvelope` session; the gateway endpoints (`/search`/`/schema`/`/call`/`/batch`/`/subscribe`) are HTTP-only and do not appear on WS; discovery via `services/list`/`services/schema` as call-protocol ops |
| [049](../../decisions/049-streaming-handler-for-subscriptions.md) | Streaming Handler for Subscription Operations | `from_openapi` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`); SSE → `BoxStream<ResponseEnvelope>` |
| [051](../../decisions/051-yaml-input-for-from-openapi.md) | YAML Input Format for from_openapi | `from_openapi` accepts JSON and YAML (`from_json`/`from_yaml`/`from_str`); `from_str` is JSON-first/YAML-fallback (correctness rule, §2); YAML dep is `yaml_serde` (maintained fork of deprecated `serde_yaml`); `to_openapi` output stays JSON (out of scope, §4) |
| [051](../../decisions/051-yaml-input-for-from-openapi.md) | YAML Input Format for from_openapi | `from_openapi` accepts JSON and YAML (`from_json`/`from_yaml`/`from_str`); `from_str` is JSON-first/YAML-fallback (defensive default, §2 amended — `yaml_serde` 0.10.x is YAML 1.2, not 1.1); YAML dep is `yaml_serde` (maintained fork of deprecated `serde_yaml`); `to_openapi` output stays JSON (out of scope, §4) |
## Relevant Open Questions
+16 -8
View File
@@ -65,8 +65,12 @@ impl OperationAdapter for FromOpenAPI {
/// fork of the deprecated `serde_yaml`). Both paths produce the same
/// `serde_json::Value`-based internal type — there is one
/// `OpenAPISpec`, not a JSON and a YAML variant. `from_str` detects
/// format by trying JSON first and falling back to YAML (correctness
/// rule — ADR-051 §2).
/// format by trying JSON first and falling back to YAML (defensive
/// default — ADR-051 §2: JSON's stricter grammar is immune to any
/// YAML-specific type interpretation, present or future; with
/// `yaml_serde` 0.10.x's YAML 1.2 core schema the coercion the original
/// rationale cited is not present, but JSON-first locks the contract
/// against a future YAML-parser swap).
pub struct OpenAPISpec {
pub info: OpenAPIInfo,
pub paths: BTreeMap<String, PathItem>,
@@ -106,7 +110,7 @@ The adapter:
1. Parses the OpenAPI document (`OpenAPISpec` — `paths`, `components`,
`$ref` resolution). Accepts JSON or YAML (ADR-051 — JSON via
`serde_json`, YAML via `yaml_serde`; `from_str` detects format
JSON-first/YAML-fallback, a correctness rule — ADR-051 §2). On parse
JSON-first/YAML-fallback, a defensive default — ADR-051 §2). On parse
failure, returns `AdapterError::SchemaParse`. The TS prior art
(`@alkdev/operations/src/from_openapi.ts`) shows the parsing patterns:
`resolveRef` for `$ref`, `resolveRefsRecursive` for nested refs,
@@ -418,10 +422,14 @@ once published, the 5-endpoint gateway shape is one-way.
- **`from_openapi` error codes are prefixed `HTTP_<status>`.** No
collision with protocol-level codes (ADR-023, review #002 W20).
- **`from_openapi` accepts JSON and YAML; `from_str` detects format
JSON-first.** JSON-first is a correctness rule (ADR-051 §2), not a
style preference. `from_str` tries JSON first (strict grammar), falls
back to YAML only if JSON parse fails. `from_json`/`from_yaml` are
the explicit constructors for callers that know the format.
JSON-first.** JSON-first is a defensive default (ADR-051 §2 as
amended): JSON's stricter grammar is immune to any YAML-specific type
interpretation. With `yaml_serde` 0.10.x (YAML 1.2 core schema) the
coercion the original rationale cited is not present, but JSON-first
locks the contract against a future YAML-parser swap. `from_str` tries
JSON first, falls back to YAML only if JSON parse fails.
`from_json`/`from_yaml` are the explicit constructors for callers that
know the format.
- **`to_openapi` is a pure projection.** It consumes the registry, does
not produce entries for it. Not an `OperationAdapter`.
- **`to_openapi` output is JSON.** The published gateway doc is served at
@@ -462,7 +470,7 @@ once published, the 5-endpoint gateway shape is one-way.
| `to_openapi` gateway pattern | [ADR-042](../../decisions/042-openapi-gateway-pattern.md) | 5 fixed gateway endpoints (search/schema/call/batch/subscribe), not one path per operation; per-caller AccessControl-filtered. Supersedes ADR-036's original `to_openapi` "paths mirror `/{service}/{op}`" clause |
| `to_openapi` published-spec versioning | [ADR-045](../../decisions/045-to-openapi-gateway-spec-versioning.md) | `info.version` semver tracks the gateway endpoint contract, not the operation set; consumers detect breaking changes via the major version |
| Streaming handler for subscriptions | [ADR-049](../../decisions/049-streaming-handler-for-subscriptions.md) | `from_openapi` `Subscription` ops register a `StreamingHandler` (`HandlerKind::Stream`); SSE response → `BoxStream<ResponseEnvelope>`; `Query`/`Mutation` stay `HandlerKind::Once` |
| YAML input + JSON-first format detection | [ADR-051](../../decisions/051-yaml-input-for-from-openapi.md) | `from_openapi` accepts JSON and YAML (`from_json`/`from_yaml`/`from_str`); `from_str` is JSON-first/YAML-fallback (correctness rule, §2); YAML dep is `yaml_serde`; `to_openapi` output stays JSON (out of scope, §4) |
| YAML input + JSON-first format detection | [ADR-051](../../decisions/051-yaml-input-for-from-openapi.md) | `from_openapi` accepts JSON and YAML (`from_json`/`from_yaml`/`from_str`); `from_str` is JSON-first/YAML-fallback (defensive default, §2 amended — `yaml_serde` 0.10.x is YAML 1.2, not 1.1; JSON-first locks the contract against a future parser swap); YAML dep is `yaml_serde`; `to_openapi` output stays JSON (out of scope, §4) |
## Open Questions
@@ -53,35 +53,54 @@ This is an additive API surface change (two-way door — constructors can be
renamed/added; nothing downstream breaks). The constructors produce the same
`OpenAPISpec`; the rest of the adapter is format-agnostic.
### 2. Format detection is JSON-first, YAML-fallback — a correctness rule, not a style preference
### 2. Format detection is JSON-first, YAML-fallback — a defensive default, not a style preference
YAML 1.1 (the version the maintained Rust YAML crates implement — see §3)
treats the bare tokens `yes`/`no`/`on`/`off`/`y`/`n` as booleans, and `1.0`
as a float. OpenAPI specs routinely have **string** fields with these values
(e.g., a query parameter named `active` with value `"yes"`, or an enum of
`["on", "off", "auto"]`). If a JSON document is parsed through the YAML
parser, `{"active": "yes"}` silently becomes `{"active": true}` — the
string is lost, the schema is wrong, and the failure is silent (no error,
just a mutated value that the forwarding handler then sends to the external
API as a boolean where a string was expected).
> **Amendment (2026-07-06):** The original §2 cited YAML 1.1
> boolean-coercion (`yes`/`no`/`on`/`off` → booleans) as a *present
> hazard* with the maintained Rust YAML crates, framing JSON-first as a
> correctness guard against silent string→boolean mutation. A probe
> during implementation verified this is factually wrong for the chosen
> dependency: `yaml_serde` 0.10.x (and the deprecated `serde_yaml` 0.9)
> implement the **YAML 1.2 core schema**, where only `true`/`false` (and
> case variants) are booleans — the bare tokens `yes`/`no`/`on`/`off`/
> `y`/`n` are plain strings. The coercion hazard the original rationale
> cited does not exist with this dependency version. The JSON-first rule
> is **retained** (Accepted ADR) — the rationale is reframed below as a
> defensive default, not a guard against a present hazard. The decision
> did not change; the rationale did.
This is a correctness footgun, not a matter of error-message quality.
`from_str` therefore tries JSON first: a JSON parse succeeds for any valid
JSON document, and JSON's stricter grammar (`"yes"` is a string, `true` is a
boolean — no ambiguity) avoids the YAML 1.1 coercion. Only if JSON parse
fails does `from_str` fall back to YAML. A YAML-only document (with `openapi:
3.0.0` at the top, no JSON braces) fails JSON parse immediately and goes to
the YAML path. The net result: JSON docs are never exposed to YAML's type
coercion, and YAML docs are parsed as YAML.
JSON's grammar is a strict subset of YAML (under YAML 1.2) and never
exhibits any YAML-specific type interpretation. Running a JSON document
through a YAML parser is *currently* safe with `yaml_serde` 0.10.x — a
JSON doc like `{"active": "yes"}` parses through the YAML path with
`"yes"` intact as a string (YAML 1.2 core schema, verified by the
`from_yaml_preserves_bare_yes_as_string_yaml_1_2_behavior` test).
JSON-first detection is therefore not guarding against a present hazard
with this dependency; it is a **defensive default that locks in the
contract against a future YAML-parser swap**. If `yaml_serde` is ever
swapped for a YAML 1.1 crate (where `yes`/`no`/`on`/`off` coerce to
booleans), or if a future `yaml_serde` version tightens its core schema
in a way that introduces type interpretation JSON doesn't have, the
JSON-first rule ensures JSON input cannot be silently mutated by the
YAML path. The contract is durable; the dependency is a two-way door
(§3).
The rule is cheap: `from_str` tries `serde_json::from_str` first (strict
grammar, no YAML-specific interpretation), and only on JSON parse failure
falls back to the YAML parser. A YAML-only document (with `openapi: 3.0.0`
at the top, no JSON braces) fails JSON parse immediately and goes to the
YAML path. The cost is one wasted parse attempt for YAML docs, paid once
at adapter-import time (not per forwarded call — see Consequences).
`from_yaml` (the explicit constructor) does not try JSON first — the caller
has declared the format. This is correct: a caller that explicitly says
"this is YAML" wants the YAML parse, including its coercion rules. If the
caller is wrong (passes JSON to `from_yaml`), the YAML parser handles it —
JSON is a syntactic subset of YAML, so it parses, but with the same
silent string→boolean coercion as the `from_str` footgun above
(`{"active": "yes"}` becomes `{"active": true}`, no error). The caller
opted in by naming the format; `from_str` exists for the unsure caller.
"this is YAML" wants the YAML parse, including whatever type
interpretation the YAML parser applies. If the caller is wrong (passes
JSON to `from_yaml`), the YAML parser handles it — JSON is a syntactic
subset of YAML, so it parses, with whatever interpretation the YAML
parser's schema applies (currently none for `yes`/`no` under YAML 1.2; a
future YAML 1.1 swap would coerce). The caller opted in by naming the
format; `from_str` exists for the unsure caller.
### 3. The YAML dependency is `yaml_serde` (the official YAML org fork of `serde_yaml`), not the deprecated `serde_yaml`
@@ -133,9 +152,13 @@ additive (a new endpoint, no breaking change to the JSON path). The
- Format detection (`from_str`) makes fetch-and-import ergonomic: a caller
that fetched a schema from a URL with no reliable Content-Type doesn't
have to sniff the format itself.
- JSON-first detection prevents the YAML 1.1 boolean-coercion footgun from
silently corrupting JSON docs. The rule is a correctness guard, not
cosmetic.
- JSON-first detection is a defensive default that locks in the
contract against a future YAML-parser swap. With `yaml_serde` 0.10.x
(YAML 1.2 core schema) the coercion hazard the original rationale
cited is not present; JSON-first nonetheless ensures JSON input is
never exposed to YAML-specific type interpretation, present or
future. The rule is cheap (one wasted parse for YAML docs, paid once
at import time) and the contract is durable.
- The maintained `yaml_serde` fork keeps the dependency off the archived
`serde_yaml`; the swap is documented so a future maintainer doesn't
re-derive why the crate name doesn't match the obvious name.
@@ -149,8 +172,9 @@ additive (a new endpoint, no breaking change to the JSON path). The
trivial — `from_openapi` runs once at adapter-import time (not per
forwarded call), so the double-parse happens once per imported service,
not per request. Callers that know the format use `from_json`/`from_yaml`
directly and pay no double-parse. The correctness benefit (never running
JSON through YAML coercion) is worth the one-time cost.
directly and pay no double-parse. The defensive benefit (JSON input never
reaches the YAML parser, immune to any YAML-specific interpretation
present or future) is worth the one-time cost.
## Assumptions
@@ -161,13 +185,19 @@ additive (a new endpoint, no breaking change to the JSON path). The
notes), both JSON and YAML constructors adapt in lockstep — the
constructor is the adapter between wire format and internal type.
2. **YAML 1.1 coercion is the only material difference for OpenAPI parsing.**
YAML 1.2 (where JSON is a true subset and `yes`/`no` are strings) would
make parse-everything-as-YAML safe. The maintained Rust crates are YAML
1.1; this ADR's JSON-first rule is the workaround. If a YAML 1.2 Rust
parser becomes the obvious choice later, the JSON-first rule becomes a
style preference rather than a correctness guard — but the rule stays
(no reason to run JSON through a YAML parser).
2. **`yaml_serde` 0.10.x implements the YAML 1.2 core schema.** Verified
by a probe during implementation: bare `yes`/`no`/`on`/`off`/`y`/`n`
are plain strings, not booleans (codified by the
`from_yaml_preserves_bare_yes_as_string_yaml_1_2_behavior` test). The
original §2 rationale cited YAML 1.1 coercion as a present hazard; it
is not, with this dependency version. JSON-first detection is retained
as a defensive default (§2 as amended): a future swap to a YAML 1.1
crate, or a future `yaml_serde` schema tightening, cannot silently
regress JSON input because JSON never reaches the YAML path under
`from_str`. If the dependency swaps to a YAML 1.1 crate, the defensive
default becomes a load-bearing correctness guard — the rule is the
same either way, which is why it is stated as a contract rather than
as a workaround for a specific crate version.
## References