--- key: api.openapi-mirrors-runtime title: 2026-07-09 — OpenAPI spec mirrors the runtime Newtonsoft serializer (#198) status: active since: '2026-07-09' supersedes: none superseded-by: none rule: The generated OpenAPI spec is made to match the runtime Newtonsoft wire contract (via `NewtonsoftSchemaNamingTransformer`), not the reverse. signals: '`CustomContractResolver`, `CustomNamingStrategy`, System.Text.Json drift · paths: `OpenApiSerializerContractTests` · issues: #198' mechanics: '`NewtonsoftSchemaNamingTransformer`, `OpenApiSerializerContractTests`' --- The generated OpenAPI document is made to follow the **runtime** JSON contract, not the reverse. Runtime `/api/*` responses are serialized by Newtonsoft via `CustomContractResolver`/`CustomNamingStrategy` (camelCase + a `FFmpegProfileId`→`ffmpegProfileId` special case + `[JsonProperty]` overrides such as `ChannelResponseModel.FFmpegProfile`→`ffmpegProfile`), while `Microsoft.AspNetCore.OpenApi` generates the spec from System.Text.Json metadata, whose camelCase drifted (`fFmpegProfileId`, `fFmpegProfile`). That drift fed the SPA the wrong key. Rather than hand-patch the spec or change the wire format (breaking clients), we added `NewtonsoftSchemaNamingTransformer` — an OpenAPI schema transformer registered on all three documents that renames each schema property through the *same* Newtonsoft contract resolver the runtime uses, so the spec matches the wire format by construction. A contract test (`OpenApiSerializerContractTests`) serializes representative DTOs through the real runtime settings and pins the spec property sets to them. Decision: **the wire format is the source of truth; the spec follows it via the real contract resolver.** This also fixed a latent SPA bug (the channel-list "FFmpeg profile" column read `fFmpegProfile` and always showed "Unassigned"). Issue #198.