Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
11c225c
fix: parse boolean subschemas wherever a schema is allowed
lightsofapollo Aug 27, 2026
51bb5be
test: round-trip generated models against source schemas
lightsofapollo Aug 27, 2026
23265bc
test: aggregate schema round-trip failures
lightsofapollo Aug 27, 2026
9a08fd0
test round-trip schemas against valid synthetic JSON
lightsofapollo Aug 27, 2026
7cd427b
fix invalid Coda required members
lightsofapollo Aug 27, 2026
42fcc3a
prevent inline schemas from overwriting components
lightsofapollo Aug 27, 2026
e3d5b15
test transitive allof alias merging
lightsofapollo Aug 27, 2026
338b284
scope inline union branches by provenance
lightsofapollo Aug 27, 2026
8b6384f
preserve broad nullable union branches
lightsofapollo Aug 27, 2026
a462205
fix contradictory discriminator mappings
lightsofapollo Aug 27, 2026
612b8b0
repair Vercel DNS record name schemas
lightsofapollo Aug 27, 2026
b932478
fix: escape boolean literal field names
lightsofapollo Aug 27, 2026
275a5d1
fix: repair boolean unions and uuid examples
lightsofapollo Aug 27, 2026
a08985e
fix: preserve nullable container values
lightsofapollo Aug 27, 2026
acca4ee
fix: honor nullable reference siblings
lightsofapollo Aug 27, 2026
239aa90
fix: preserve nullable union references
lightsofapollo Aug 27, 2026
96e810d
fix: widen integers from schema bounds
lightsofapollo Aug 27, 2026
3c4c497
fix: preserve null-only enum semantics
lightsofapollo Aug 27, 2026
b09acc8
fix: preserve binary JSON field shape
lightsofapollo Aug 27, 2026
6d77397
fix: preserve multi-branch union nullability
lightsofapollo Aug 27, 2026
efa7c71
fix: preserve constrained dynamic object keys
lightsofapollo Aug 27, 2026
cb54e48
fix: preserve scalar allOf carriers
lightsofapollo Aug 27, 2026
1c3c1f8
fix: preserve unions in allOf objects
lightsofapollo Aug 27, 2026
e6c1e92
fix(specs): repair Gitpod required wire names
lightsofapollo Aug 27, 2026
a335cc0
fix(models): preserve optional nullable wire presence
lightsofapollo Aug 27, 2026
3f2b2c3
fix(types): preserve RFC3339 time offsets
lightsofapollo Aug 27, 2026
5b423e0
fix(validation): align implicit object schemas
lightsofapollo Aug 27, 2026
f6ffb10
fix(unions): preserve all discriminator wire values
lightsofapollo Aug 27, 2026
9c1c48e
fix(roundtrip): exclude raw binary contracts
lightsofapollo Aug 27, 2026
7cceb3d
fix(unions): add structural discriminator fallback
lightsofapollo Aug 27, 2026
c467148
fix(unions): share flattened discriminator input
lightsofapollo Aug 28, 2026
c818349
fix(unions): select complete oneof object shapes
lightsofapollo Aug 28, 2026
f3135c9
fix(analysis): preserve nested inline provenance
lightsofapollo Aug 28, 2026
0ac5e21
fix(analysis): preserve closed empty objects
lightsofapollo Aug 28, 2026
e77f800
test(unions): cover heterogeneous array aliases
lightsofapollo Aug 28, 2026
0379552
fix(discriminators): reject invalid mapping tags
lightsofapollo Aug 28, 2026
6b5f46b
fix(roundtrip): align synthetic discriminator branches
lightsofapollo Aug 28, 2026
eada3eb
fix(fixtures): align Letta discriminator contracts
lightsofapollo Aug 28, 2026
378af54
fix(fixtures): constrain Storyden discriminator tags
lightsofapollo Aug 28, 2026
fa94814
fix(discriminators): preserve explicit stainless consts
lightsofapollo Aug 28, 2026
b5c6bca
fix(roundtrip): synthesize implicit discriminator tags
lightsofapollo Aug 28, 2026
2135f2d
fix(unions): preserve complete anyof object payloads
lightsofapollo Aug 28, 2026
42acf9e
fix(allof): project sibling fields for union matching
lightsofapollo Aug 28, 2026
9b24159
fix(unions): retain numeric refinements for dispatch
lightsofapollo Aug 28, 2026
b197161
fix(discriminators): preserve anyof fallback semantics
lightsofapollo Aug 28, 2026
48a8679
fix(unions): allow canonical additions in anyof
lightsofapollo Aug 28, 2026
fc3f82e
fix(discriminators): prefer standard enum domains
lightsofapollo Aug 28, 2026
8e9c861
test(roundtrip): bound generated test stack
lightsofapollo Aug 28, 2026
ce821a2
fix(unions): compare json numbers by value
lightsofapollo Aug 28, 2026
70ea47d
fix(oneof): handle empty literal domains
lightsofapollo Aug 28, 2026
4d28f2d
fix(oneof): defer nullable literals to serde
lightsofapollo Aug 28, 2026
08d3f48
test(roundtrip): validate selected anyof branches
lightsofapollo Aug 28, 2026
71e12c3
fix(examples): ignore union metadata extensions
lightsofapollo Aug 28, 2026
b4dc030
test(unions): assert lossless boolean anyof dispatch
lightsofapollo Aug 28, 2026
1183648
test(discriminators): assert lossless collision fallback
lightsofapollo Aug 28, 2026
0b07523
fix(unions): emit rust 2021-compatible dispatch
lightsofapollo Aug 28, 2026
37f8fea
test(openai): assert tri-state nullable fields
lightsofapollo Aug 28, 2026
31c0381
test(storyden): use generated plugin mode type
lightsofapollo Aug 28, 2026
ee2c108
refactor(unions): satisfy strict clippy
lightsofapollo Aug 28, 2026
8e67619
test(ci): align compatibility fixtures with generated models
lightsofapollo Aug 28, 2026
b32c7ff
test(ci): cache uuid for offline scratch crate
lightsofapollo Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,52 @@ when correcting output that was wrong or incomplete on the wire.

## [Unreleased]

### Added

- 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 anthropic` use the same gate and report sample and
skip coverage.

### Changed

- **Breaking (generated API).** Structs used as discriminated-union variants
retain their discriminator fields, so constructing one directly now requires
the same tag its standalone component schema requires. Parent unions perform
explicit discriminator-directed Serde dispatch instead of stripping the
field and relying on an internally tagged derive. This keeps direct values,
arrays, and union payloads on one schema-valid wire shape.

### Fixed

- Required nullable fields serialize `None` as explicit JSON `null` instead of
omitting a key listed by the schema's `required` array. Nullable component
schemas referenced by a property now propagate that nullability to the field.
- A composition nested as one branch of an outer `anyOf` is retained as a named
Rust union variant instead of being silently dropped.

- Boolean subschemas (`true` and `false`) parse wherever JSON Schema 2020-12
allows one — a property, a `$defs` entry, `not`, `if`/`then`/`else`,
`contains`, `propertyNames`, `patternProperties`, `dependentSchemas`, a
`oneOf` branch. `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).

`true` generates `serde_json::Value` and `false` a value that cannot occur —
both reported as faithful by `--report-untyped`. In a union, a `true` branch
makes the union unconstrained and a `false` branch is dropped, so
`oneOf: [A, false]` is `A`.
- 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 like `2.5` is still an
error.

Together these take the vendored JSON Schema 2020-12 corpus from 38 parse
failures to zero, with no round-trip loss.


## [0.14.0] - 2026-08-27

### Added
Expand Down
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,23 @@ The full corpus generates and compile-checks 55 OpenAPI documents and can take
several minutes. CI runs a fast generation tier on pull requests and the full
compile tier weekly or on manual dispatch.

Compile runs also generate deterministic, schema-valid JSON samples for each
representable component model. Each sample is independently validated against
the source OpenAPI JSON Schema, hydrated into the generated Rust type,
serialized, validated again, and round-tripped a second time to require a stable
wire representation. Start with one production spec while iterating:

```bash
scripts/spec-compile.sh anthropic
```

The full `scripts/spec-compile.sh` command applies the same check across the
55-spec compile suite. Its summary reports component and sample coverage plus
explicit schema skips. Set `SPEC_COMPILE_SCHEMA_ROUNDTRIP=0` only when isolating
an unrelated compile failure; parse-only runs skip model round trips because
they do not compile generated Rust. Failed scratch crates and logs are retained
under `tmp/spec-compile/`.

## Compatibility expectations

Until 1.0, a minor release may correct generated Rust APIs that were incomplete
Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ serde_path_to_error = "0.1.20"
serde_yaml = "0.9"
insta = { version = "1.41", features = ["yaml"] }
tempfile = "3.0"
uuid = { version = "1", features = ["serde"] }

[features]
default = ["cli"]
Expand All @@ -82,6 +83,11 @@ name = "file-beads"
path = "src/bin/file-beads.rs"
required-features = ["internal-tools"]

[[bin]]
name = "schema-roundtrip"
path = "src/bin/schema-roundtrip.rs"
required-features = ["internal-tools"]

[lints.clippy]
unwrap_used = "deny"
expect_used = "warn"
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,14 @@ cargo insta review # review snapshot diffs
scripts/spec-compile.sh # generate + cargo-check every spec in specs/ (full corpus)
```

`scripts/spec-compile.sh` also synthesizes valid JSON instances from each
source component schema, hydrates the exact generated Rust model, serializes it
back to JSON, validates that output against the same schema, and requires a
stable second round trip. The summary reports tested components, generated
samples, and explicit skips. Pass one or more spec names (for example,
`scripts/spec-compile.sh anthropic`) for a focused trial before running the
full 55-spec compile suite.

The compile tiers are intentionally different:

- Every pull request and push to `main` generates all 55 supported OpenAPI
Expand Down
3 changes: 2 additions & 1 deletion examples/anyof_unions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,12 +107,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
" - {}: {:?}",
name,
match &schema.schema_type {
openapi_to_rust::analysis::SchemaType::Union { variants } => {
openapi_to_rust::analysis::SchemaType::Union { variants, .. } => {
format!("Union(variants: {})", variants.len())
}
openapi_to_rust::analysis::SchemaType::DiscriminatedUnion {
discriminator_field,
variants,
..
} => {
format!(
"DiscriminatedUnion(discriminator: {}, variants: {})",
Expand Down
1 change: 1 addition & 0 deletions examples/debug_beta_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if let openapi_to_rust::analysis::SchemaType::DiscriminatedUnion {
discriminator_field,
variants,
..
} = &schema.schema_type
{
println!("\nDiscriminator field: {discriminator_field}");
Expand Down
1 change: 1 addition & 0 deletions examples/discriminated_unions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
openapi_to_rust::analysis::SchemaType::DiscriminatedUnion {
discriminator_field,
variants,
..
} => {
format!(
"DiscriminatedUnion(discriminator: {}, variants: {})",
Expand Down
1 change: 1 addition & 0 deletions examples/discriminator_mappings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
openapi_to_rust::analysis::SchemaType::DiscriminatedUnion {
discriminator_field,
variants,
..
} => {
let variant_info: Vec<String> = variants
.iter()
Expand Down
3 changes: 2 additions & 1 deletion examples/inline_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
openapi_to_rust::analysis::SchemaType::DiscriminatedUnion {
discriminator_field,
variants,
..
} => {
let variant_info: Vec<String> = variants
.iter()
Expand All @@ -134,7 +135,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
variant_info.join(", ")
)
}
openapi_to_rust::analysis::SchemaType::Union { variants } => {
openapi_to_rust::analysis::SchemaType::Union { variants, .. } => {
let variant_info: Vec<String> =
variants.iter().map(|v| v.target.to_string()).collect();
format!("Union(variants: [{}])", variant_info.join(", "))
Expand Down
2 changes: 1 addition & 1 deletion examples/openai_patterns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
openapi_to_rust::analysis::SchemaType::Object { properties, .. } => {
format!("Object({} properties)", properties.len())
}
openapi_to_rust::analysis::SchemaType::Union { variants } => {
openapi_to_rust::analysis::SchemaType::Union { variants, .. } => {
let variant_info: Vec<String> =
variants.iter().map(|v| v.target.to_string()).collect();
format!("Union(variants: [{}])", variant_info.join(", "))
Expand Down
1 change: 1 addition & 0 deletions examples/server-anthropic-messages/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fn messages_unary() -> MessagesPostResponse {
content: vec![gen::ContentBlock::TextBlock(gen::ResponseTextBlock {
citations: None,
text: "hello (unary)".into(),
r#type: gen::ResponseTextBlockType::Text,
})],
id: "msg_demo".into(),
model: gen::Model::Custom("claude-demo".into()),
Expand Down
4 changes: 2 additions & 2 deletions examples/server-openai-responses/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Example: host a perfect-replica of OpenAI's `POST /v1/responses`.
//!
//! Exercises both branches of the typed response enum:
//! - `body.stream == Some(true)` → `OkStream(Sse<...>)`
//! - `body.stream.flatten().unwrap_or(false)` → `OkStream(Sse<...>)`
//! - otherwise → `Ok(Response)` (single JSON body)
//!
//! Run:
Expand Down Expand Up @@ -35,7 +35,7 @@ struct AppState;
#[async_trait::async_trait]
impl ResponsesApi for AppState {
async fn create_response(&self, body: CreateResponse) -> CreateResponseResponse {
if body.stream == Some(true) {
if body.stream.flatten().unwrap_or(false) {
create_response_streaming()
} else {
create_response_unary(body)
Expand Down
1 change: 1 addition & 0 deletions examples/test_nested_discriminators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
openapi_to_rust::analysis::SchemaType::DiscriminatedUnion {
discriminator_field,
variants,
..
} => {
let variant_info: Vec<String> = variants
.iter()
Expand Down
Loading
Loading