All notable changes are recorded here. This project follows semantic versioning, with one pre-1.0 qualification: a minor release may change generated Rust APIs when correcting output that was wrong or incomplete on the wire.
0.15.0 - 2026-08-28
- Optional, nullable properties now generate
Option<Option<T>>so Serde can distinguish a missing key (None) from an explicit JSONnull(Some(None)) and a value (Some(Some(value))). Existing constructors and comparisons need one moreSome; when only the value matters, preferfield.flatten()(for example,body.stream.flatten().unwrap_or(false)). Required nullable properties remainOption<T>, but now serializeNoneas the required JSONnullinstead of omitting the key. - Structs used as discriminated-union variants retain their discriminator fields. Constructing a variant payload directly must now initialize the tag field with its generated enum value. Parent unions use explicit discriminator-directed Serde dispatch instead of stripping the tag with an internally tagged derive.
- Correct schema information can change generated names and field types.
Inline and nested types now use path/provenance-aware names; nullable
containers and references keep their wrappers; scalar
allOf, combined object/union compositions, constrainedadditionalProperties, and nested unions no longer fall back to incomplete carriers orserde_json::Value. Regenerate code and update imports, struct literals, and enum pattern matches rather than expecting the0.14.xmodel shape. - Integer types are selected from the effective schema bounds and may widen
(for example, from
i32toi64oru64). With either typed date strategy, JSON Schemaformat: timenow remainsStringbecause RFC 3339 full-time includes an offset thatchrono::NaiveTimeandtime::Timecannot preserve. Binary model fields configured asVec<u8>orbytes::Bytesstill use those Rust types but now serialize as JSON strings instead of byte arrays.
oneOfdeserialization now requires exactly one schema-valid object branch; ambiguous and no-match payloads are rejected.anyOfpreserves the complete input object and chooses a schema-valid branch deterministically instead of silently dropping keys while trying variants. Applications that depended on the previous first-Serde-match behavior should handle the resulting decode errors explicitly.- Discriminator mappings,
const/enumtag domains, required tags, and multi-value tag domains are enforced. A mapped branch may fall back to structural matching only when the preferred branch does not validate, and a missing tag may fall back only where the branch schema permits it. Payloads previously accepted with contradictory, invented, or stripped tags can now be rejected.
openapi::SchemagainedBool(bool), andopenapi::SchemaDetails::{minimum, maximum}changed fromOption<f64>toOption<serde_json::Number>so wide integer bounds are not rounded.analysis::SchemaTypegainedNullable; itsUnionandDiscriminatedUnionvariants gainedexclusive.analysis::PropertyInfogainedsynthesized_required, whileanalysis::UnionVariantgaineddiscriminator_values,preferred_discriminator_values,discriminator_field_declared, anddiscriminator_field_required. Downstream exhaustive matches and public-struct literals must include the new variants and fields.
- The 55-spec compile gate now generates deterministic JSON instances for
representable component schemas, validates them against the source JSON
Schema, hydrates and serializes the exact generated Rust models, validates
their output, and requires a stable second round trip. Targeted runs such as
scripts/spec-compile.sh anthropicuse the same gate and report sample and skip coverage. The completed corpus run covered 28,139 of 29,045 components with 103,031 schema-valid samples and 906 explicit skips. - The
internal-toolsfeature now exposes theschema-roundtripbinary and module used by the corpus gate; the validation-onlyuuiddependency remains outside normal CLI and library builds.
- Schema round-trip validation is enabled by default for non-parse-only
scripts/spec-compile.shruns. SetSPEC_COMPILE_SCHEMA_ROUNDTRIP=0only to isolate an unrelated generation or compile failure.
-
Required nullable fields serialize
Noneas explicit JSONnullinstead of omitting a key listed by the schema'srequiredarray. Nullable component schemas referenced by a property now propagate that nullability to the field. -
A composition nested as one branch of an outer
anyOfis retained as a named Rust union variant instead of being silently dropped. -
Boolean subschemas (
trueandfalse) parse wherever JSON Schema 2020-12 allows one — a property, a$defsentry,not,if/then/else,contains,propertyNames,patternProperties,dependentSchemas, aoneOfbranch.properties: {extra: true}is how a spec says "this key exists, any value"; one of those anywhere in a document used to fail the whole thing with "data did not match any variant of untagged enum Schema" (#63).truegeneratesserde_json::Valueandfalsea value that cannot occur — both reported as faithful by--report-untyped. In a union, atruebranch makes the union unconstrained and afalsebranch is dropped, sooneOf: [A, false]isA. -
Integer keywords written as decimals —
maxItems: 2.0, which JSON Schema permits and the 2020-12 suite exercises — are read as the counts they are rather than rejecting the document. A fractional value like2.5is still an error.Together these take the vendored JSON Schema 2020-12 corpus from 38 parse failures to zero, with no round-trip loss.
-
Nullable values are preserved through reference siblings, array items, additional-property values, multi-branch unions,
allOf, and null-only enum schemas instead of being rejected or collapsed into a non-null type. -
Inline schemas use collision-safe provenance paths, and components are no longer overwritten by same-named inline types. Scalar and union carriers in
allOfkeep all declared information, including sibling object properties. -
Closed empty objects, constrained dynamic object keys, boolean literal field names, wide integer domains, binary strings, and RFC 3339 time offsets now round-trip without changing their schema-defined JSON shape.
0.14.0 - 2026-08-27
openapi-to-rust generate --report-untypedreports every generated field that carriesserde_json::Value, grouped by why, and marks each faithful (the schema declared an unconstrained value) or recoverable (the generator dropped type information the schema carried).--jsonemits the findings with paths for tooling.scripts/untyped-census.shruns that acrossspecs/and rewritestests/conformance/untyped-report.md, so a change that alters which fields get typed shows its corpus delta in review;--checkfails when the report is stale.- Generated extensible enums now expose
as_strand implementDisplayandAsRef<str>, matching what generated string enums already had. Needed because a multipart form field, query parameter, or header can now be one.
-
Breaking (generated API). Positional item schemas — 2020-12
prefixItemsand the draft-04items: [A, B]spelling — now generate typed Rust tuples instead ofVec<serde_json::Value>, when the spec pins the array's length (minItems/maxItems,items: false, oradditionalItems: false). A[string, integer]pair becomes(String, i64); a$refposition keeps its named type, and an inline object position is hoisted to one. When no extras are allowed but the length varies and every position shares a type, the array becomesVec<T>.An open
prefixItemsstill generatesVec<serde_json::Value>on purpose: it permits extra elements of any type, and a fixed-arity tuple would reject payloads the spec allows (#62). -
Breaking (library API).
SchemaTypegainedUntyped { shape, reason }, which replaces the stringly-typedPrimitive { rust_type: "serde_json::Value" }fallbacks and carries why a value could not be typed, andTuple { element_types }for fixed-length positional items.SchemaType::Objectgained avariantfield holding a union declared alongside its properties. Exhaustive matches and struct literals need updating. -
Breaking (library API).
Schema::OneOfgained aschema_typefield, so a union that also declarestypekeeps it;Itemsgained aBoolvariant for 2020-12 boolean schemas.SchemaAnalysis::untyped_fields()returns the census.
-
Schemas that carried enough information to type no longer degrade to
serde_json::Value. Across the 57-spec corpus this types every field the census could attribute to a dropped type — 3,347 of them — taking the total untyped surface from 13,226 fields to 8,829, all of which are schemas that genuinely declared an unconstrained value (#62, #65):anyOf: [$ref, {type: object, nullable: true}]— how OData spells "that type, or null" — becomesOption<T>instead of an untyped union (2,127 fields in Microsoft Graph alone);- an inline object, union, enum, or merged
allOfin a field or element position is hoisted to a named type instead of being dropped by the generator, which could not render one inline; allOfwith a single member takes that member's type, andallOfinside array items is analyzed instead of ignored;- a
$refto any local JSON Pointer resolves — a parameter's schema, one member of another schema's composition — not only#/components/schemas/<name>; type: nullbecomes(), which serde reads and writes asnull;- a union whose branch list is empty takes the schema's declared type; a
union of one branch is that branch; branches differing only in constraints
share one type; branches that only alternate
requireddescribe the object their properties declare; and branches that are local pointers are expanded before the union is built; - a schema declaring
propertiesand a union — "these fields, and one of these shapes" — generates the struct with the union in a#[serde(flatten)]field, instead of discarding both halves (#65).
-
items: falseanditems: true— 2020-12 boolean schemas, and the canonical way to close a tuple — now parse instead of failing the document with "data did not match any variant of untagged enum Schema" (#62). -
Reference cycles that run through a synthesized type — a hoisted union, a hoisted property type — are detected, so the generated enum is boxed instead of having infinite size. Typing a field that was previously
serde_json::Valuecan close a cycle the untyped value had been breaking by accident.
0.13.0 - 2026-08-26
- Breaking (library API).
SchemaDetails.itemsis nowOption<Items>rather thanOption<Box<Schema>>, so the keyword can hold either the 2020-12 single-schema form or a draft-04 positional tuple. Read the former throughSchemaDetails::item_schema()and the latter — unified withprefixItems— throughSchemaDetails::positional_items(). Generated code is unaffected. - Breaking (library API).
GeneratorErrorgained aParseErrorAtvariant carrying the JSON Pointer of a located parse failure. Exhaustive matches over the enum need a new arm.
- The draft-04 positional tuple form
items: [A, B]— still emitted underopenapi: "3.1.0"by FastAPI/pydantic v1 — now parses instead of failing the whole document, generating what the 2020-12 spellingprefixItems: [A, B]generates. Generated Axum validators receive the canonical spelling, so the positions are actually checked at runtime (#60). - Document parse failures now name the offending node by JSON Pointer, e.g.
Failed to parse OpenAPI spec at #/components/schemas/Body/properties/pair/items, instead of reporting only "data did not match any variant of untagged enum Schema" with no way to find it in a large spec (#60).
0.12.3 - 2026-08-22
- OpenAPI 3.1 schemas with multiple non-null types now generate proper Rust unions instead of being treated as nullable versions of their first type; array and object members retain their declared shapes.
0.12.2 - 2026-08-18
- Schemas that pair
allOfwith a redundant siblingtype: objectare now parsed as compositions instead of plain typed objects, so the composed members are merged into the generated struct rather than dropped.
0.12.1 - 2026-08-07
- Distinct OpenAPI component keys that normalize to the same Rust identifier are deterministically disambiguated instead of silently dropping a model; references, discriminator mappings, dependencies, and operation schemas are rewritten to the emitted names.
- Implicit discriminators are selected only when every union branch has a unique constant value. Ambiguous unions now remain untagged, preserving nested constant fields so Serde can distinguish branches at runtime.
- The real-world compile corpus now includes the OpenCode OpenAPI 3.1 document.
0.12.0 - 2026-07-29
This release broadens the set of real-world protocols that generated clients
and servers can represent, and replaces the old inline SSE helpers with a
reusable typed transport. Regenerated clients may have new request/response
types, a new ApiError::raw_body field, and newer HTTP dependencies; review
generated-code diffs when upgrading.
- SSE-enabled output now includes a standalone
sse.rstransport withSseClient, rawSseEvent<String>streams, typed JSONSseEvent<T>streams, and the backwards-compatible payload-only stream. Event name, ID, and server-providedretry:delay remain available to callers. - SSE streams can reconnect with bounded exponential backoff, honor the
server's
retry:value, and send the most recently observed event ID asLast-Event-ID. HTTP 429, 5xx, connection failures, and early EOF are retryable; invalid content types and other terminal errors are not. The generated runtime was exercised against live OpenAI- and Anthropic-compatible streaming endpoints. - Flat
multipart/form-dataobject schemas generate typed reqwest clients and Axum extractors, including required and optional binary/scalar fields, configured body limits, validation, and deterministic rejection of shapes the generator cannot encode symmetrically. - Generated clients and servers support bounded binary and text request bodies,
including
application/octet-stream,application/pdf,text/plain, XML, andapplication/jwt. Non-JSON responses preserve exact bytes, andApiError<E>::raw_bodyexposes the unmodified response alongside its lossy text rendering. - Buffered client responses and SSE error responses have an 8 MiB default
limit, configurable through
http_client.max_response_body_bytesand the generated runtime builders. Oversized bodies returnResponseTooLargewithout buffering beyond the limit. - Component-level Request Body Object references are resolved during operation
analysis, so
requestBody: { $ref: ... }participates in normal client and server generation. - The checked-in corpus now includes Storyden, and the documentation includes a dated Progenitor workflow comparison with a reproducible compile benchmark.
- Generated HTTP dependencies now target
reqwest0.13,reqwest-middleware0.5,reqwest-retry0.9,reqwest-tracing0.7, andthiserror2. Required reqwest and middleware features are inferred from the selected operations, including query, form, multipart, streaming, and JSON usage; rustls builds use reqwest 0.13'srustlsfeature. - AWS query-protocol operations can use bounded nested object/array form encodings, while simple array headers and path parameters with literal prefixes or suffixes now have matching typed client/server serialization.
- Text and binary response media produce
Stringandbytes::Bytesvalues instead of being forced through JSON or lossy UTF-8 conversion. Generated server response variants retain their declared media type. - The real-world corpus contains 56 documents: 55 supported OpenAPI specs and
one intentionally skipped Swagger 2.0 Gitea document. The ordinary full tier
compiles 54 and reports Microsoft Graph as generate-only because its generated
crate exceeds CI memory;
SPEC_COMPILE_FORCE_CHECK=1enables local compile-verification on larger machines.
- Bodyless operations that declare request-content semantics send
Content-Length: 0; ordinary methods without content semantics remain unchanged, and optional bodies add the header only when absent. - Media selection recognizes PDF as binary and XML,
+xml, and JWT as text, prefers schema-bearing vendor JSON over schema-less canonical JSON, and rejects wildcard or proprietary request media instead of emitting an invalidContent-Type. - Recursive annotation-only
allOfreferences are aliases again, avoiding expansion overflows, and generated server code consistently uses canonical Rust model names while avoiding response-enum name collisions. - Spec parsing tolerates literal tabs in YAML block-scalar prose and ignores
extension scalars parked inside
paths. AWS route fragments are stripped and synthetic webhook paths receive a leading slash. - Server-side validation normalizes common Java POSIX, ECMA Unicode, and legacy octal regex syntax; unsupported look-around/backreference patterns no longer abort generation. Component keys that resemble JSON Schema keywords are namespaced in generated validation bundles.
- Portable scratch directories and complete generated dependency fragments keep the expanded corpus and packaged examples compiling on clean CI runners.
0.11.0 - 2026-07-27
Nearly everything here was found by generating a client from RunPod's published OpenAPI document and exercising it against the live API. The spec was accurate; the generator was not. Two of these defects broke real calls outright, and both would have passed any amount of spec-diffing.
format: floatnow maps tof64instead off32. JSON carries no binary32, so the declared format describes the server's storage rather than the transport: a value sent as0.03survives inf64but becomes0.029999999329447746throughf32, which matters when the field is money. Setfloat_precision = "f32"under[generator.types]to map strictly by declared format.--types-conservativekeeps the literalf32mapping.
- Parameter-level inline enums honor
x-enum-varnames. Schema-level enums already did, so the same enum produced different Rust variant names depending on whether it lived incomponents.schemasor on a parameter. A varnames array whose length disagrees withenumis ignored rather than applied to a prefix. - Properties that are both
requiredand nullable via OpenAPI 3.1'stype: ["X", "null"]now generateOption<T>instead of a bareT, in plain object schemas and inallOf-composed ones alike. Previously such a client compiled and then failed to deserialize the first real response containingnull. All three nullability spellings (nullable: true, the 3.1 type array, and ananyOf/oneOfnull branch) now route through one helper. - Client operations whose only success content is
text/event-streamreturn afutures_util::Streamof bytes instead of(). They previously buffered the response with.text(), which never returns on a live SSE stream and hung the caller's task indefinitely. - Generated clients default
base_urlto the document'sservers[0].urlwhen configuration does not set one, soHttpClient::new()targets the real API instead of an empty string. Explicit configuration still wins; relative and templated server URLs are ignored. - The
Default(..)per-operation error variant is now constructed for responses matched by the spec'sdefaultresponse. It was previously declared but unreachable, so a typeddefaultbody still surfaced astyped: None. - Specs with
multipart/form-dataoperations now request reqwest'smultipartfeature inREQUIRED_DEPS.toml. The feature was enabled forreqwest-middlewarebut not forreqwestitself, so generated file-upload clients failed to compile.
- The
full-spec-compileCI tier passes again. It had been killed with SIGTERM roughly 25 minutes into a 240-minute budget, onmainas well as branches. The cause was memory, not time or disk:microsoft-graphgenerates 2.4M lines from 16,153 operations and peaks at ~14.3 GB in a single rustc process against a 16 GB runner. It is now generated but not compile-checked, reported in its own bucket so a green run is never mistaken for full corpus verification.SPEC_COMPILE_FORCE_CHECK=1checks it where there is headroom.
0.10.0 - 2026-07-26
- Every-PR compatibility coverage for the generated Anthropic Messages server through the pinned official Python SDK, including unary and SSE responses.
- Regenerated server response enums now use the declared status in bodyless and SSE variant names and require a runtime status for wildcard/default variants. This is a source-breaking correction for existing server trait implementations.
- Config-driven
server listandserver addnow applygenerator.schema_extensions, so overlay-provided operations and SSE media types match generation. - Schema extensions accept the documented JSON, YAML, and YML formats with path-rich parse errors.
- Generated server response enums retain reusable Response Object references,
including structurally compatible local refs stored outside
components.responses, plus bodyless status codes, vendor/problem JSON media types, SSE status codes, and runtime status values for wildcard/default responses. - Server generation rejects response sets that contain only unsupported media types and reports normalized Rust identifier collisions between distinct tags.
- Server example tests use Cargo's current integration-test binary instead of
a potentially stale hard-coded
target/debugexecutable.
0.9.1 - 2026-07-26
- Restored client generation for operations whose selected JSON or form request content declares no schema. These operations keep their historical no-body client signature, while server generation fails with an actionable error because there is no request contract to validate.
0.9.0 - 2026-07-26
- Default-on request validation for generated Axum servers, compiled offline from the selected OpenAPI/JSON Schema contract with bounded body and error limits.
- Sanitized
application/problem+jsonresponses for malformed input (400), oversized bodies (413), undeclared media types (415), schema violations (422), and generated contract mismatches (500). - Typed validation for supported path, query, header, cookie, JSON, and
form-urlencoded inputs, plus lazy
ApiError::problem_details()decoding in generated clients without replacing documented typed errors. - Live generated-client/server and independent-client compatibility tests that verify status codes, stable JSON Pointer locations, redaction, deterministic error caps, and handler isolation.
- Generated servers and exact dependency fragments now target Axum 0.8 and its
{parameter}route syntax consistently. Trait implementations use the directasync-traitdependency because Axum 0.8 no longer re-exports the attribute macro (#38). - Server request constraints now use
jsonschema0.49 with remote file/HTTP resolution disabled. Model types remain free of validation derives. - Unsupported aggregate parameter encodings and selected multipart, text, or octet-stream server bodies fail generation explicitly instead of being silently omitted.
- OpenAPI schema serialization now omits absent optional keywords while
preserving an explicit
const: null, preventing missing keywords from becoming unintended null constraints or otherwise disabling validation. - Vendor JSON media types are retained end to end, and generated servers match
the media type selected from the operation rather than accepting every
application/*+jsonbody.
0.8.0 - 2026-07-19
- An in-browser WASM playground at
openapi-to-rust.dev/playground:
paste a spec or fetch one by URL and get the exact generated file set —
byte-identical to
openapi-to-rust generate <SOURCE>— with a downloadable runnable crate. - A default-on
clifeature gating clap and reqwest. With--no-default-featuresthe library compiles onwasm32-unknown-unknown; URL policy and spec parsing moved into the sharedspec_sourcemodule.
ApiErrordisplay output now bounds large response-body previews and includes typed error details or typed-body parse failures when available (#29).
0.7.0 - 2026-07-17
- Direct generation from a local OpenAPI document or bounded HTTPS URL:
openapi-to-rust generate <SOURCE>. openapi-to-rust init <SOURCE>, plus deterministic--dry-run,--check,--quiet, and--jsongeneration modes.- Optional
[client].operationsselection and model pruning shared with the server operation scope. Defaultfor all-optional request models, required-field constructors, fluent optional setters, and opt-in operation builders.- A complete
REQUIRED_DEPS.tomlfor the exact generated output. base64_url_unpaddedas a spec-wideformat: bytestrategy for RFC 7515 URL-safe, unpadded data.- Contributor, support, security, conduct, issue-form, and pull-request scaffolding, plus a docs.rs library overview and compile-checked example.
- The public CLI as Cargo's default binary, so plain
cargo run -- ...works even though feature-gated internal maintenance binaries are declared.
- Array items with an inline string enum now generate a named enum
(
{Parent}Item) instead of collapsing toVec<String>, includinganyOf-nullable arrays and typeless OpenAPI 3.1 enums (#33). - README compatibility, corpus, and conformance claims; pull-request and scheduled full-corpus CI tiers; and release preflight checks.
- Canonical
[generator.types]configuration parsing, strict unknown-field rejection, config-relative paths, and actionable migration errors. cargo install --locked openapi-to-rustpackaging: only the public CLI is installed, packaged inputs are complete, and obsolete duplicate dependency versions were removed.- Server query extraction now mirrors generated client serialization for typed form, repeated-array, comma-delimited, and deep-object query parameters.
- Generated client requests now support non-JSON bodies, optional bodies, typed headers, path encoding, and collision-safe operation signatures.
0.6.0 - 2026-07-13
- OpenAPI
style/explode-aware client serialization for object and array query parameters, including form-exploded objects, comma-joined form values, deep-object parameters, and repeated arrays. - Shared-target full-corpus compile tooling in
scripts/spec-compile.sh.
- Regenerated client signatures use typed objects and arrays instead of opaque
Option<impl AsRef<str>>arguments for the supported query styles. This is a source-breaking correction for regenerated pre-1.0 clients.
0.5.3 - 2026-07-11
- Added working generated serde codecs for
time::Dateandtime::Time.
0.5.2 - 2026-07-11
- Honored integer and number formats for query and path parameters.
0.5.1 - 2026-07-07
- Restricted the crates.io package to the source, manifest, README, and license.
0.5.0 - 2026-07-07
- Opt-in Axum server generation with operation selectors, per-tag traits, typed response enums, router factories, SSE response support, and model pruning.
- OpenAPI 3.1 modeling and experimental parsing for selected OpenAPI 3.2 fields and methods.
- Typed scalar strategies, typed
additionalProperties, operation-level typed errors, strict extension parsing, webhook ingestion, and SSE auto-detection. - End-to-end OpenAI Responses and Anthropic Messages server examples.
- Numerous real-spec generation failures involving operation identifiers, signed enum values, recursive unions, parameter collisions, optional request bodies, range response codes, and path-segment encoding.