Skip to content

Conversation

timvw
Copy link
Contributor

@timvw timvw commented Sep 6, 2025

Summary

This PR implements true anyOf support for the Rust generator, properly differentiating it from oneOf semantics as defined in the OpenAPI specification.

Key Changes

  • anyOf: Now generates structs with optional fields (OR semantics - one or more schemas can match)
  • oneOf: Continues to generate enums (XOR semantics - exactly one schema must match)
  • allOf: Generates structs with required fields (AND semantics - all schemas must match)

OpenAPI Specification Compliance

  • oneOf spec: "Validates the value against exactly one of the subschemas"
  • anyOf spec: "Validates the value against any (one or more) of the subschemas"

Implementation Details

  • anyOf schemas generate structs with optional fields for each subschema
  • Includes validate_any_of() method to ensure at least one field is set
  • Properly handles composition where multiple schemas can be valid simultaneously

Testing

  • Comprehensive test suite demonstrating behavioral differences
  • Tests for simple types, objects, and complex nested schemas
  • Validation of OR semantics for anyOf vs XOR semantics for oneOf

Documentation

  • Added detailed documentation explaining the semantic differences
  • Cross-language comparison showing how other generators handle these constructs
  • Analysis of spec compliance and ambiguity handling strategies

Breaking Changes

  • anyOf now generates structs instead of enums (semantically correct but breaking)
  • Migration guide included in documentation

Related Issues

  • Fixes incorrect anyOf semantics in Rust generator
  • Aligns with OpenAPI/JSON Schema specifications

PR checklist

  • Read the contribution guidelines.
  • Pull Request title clearly describes the work in the pull request and Pull Request description provides details about how to validate the work.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
  • File the PR against the correct branch: master
  • @mention the technical committee members: @frol @farcaller @richardwhiuk @paladinzh @jacob-pro

wing328 and others added 5 commits September 6, 2025 21:54
- anyOf now generates struct with optional fields instead of enum
- Each anyOf option becomes an optional field prefixed with 'as_'
- Added validate_any_of() method to ensure at least one field is set
- Maintains semantic difference between anyOf (OR) and oneOf (XOR)
- oneOf continues to use untagged enums (exactly one match)
- anyOf uses struct with optional fields (one or more matches possible)

BREAKING CHANGE: anyOf schemas now generate different code structure.
Previously generated enums, now generates structs with optional fields
to properly support OpenAPI anyOf semantics where multiple schemas can
validate simultaneously.
- Added comprehensive test spec covering various oneOf/anyOf scenarios
- Tests simple primitives, complex objects, and nested schemas
- Tests oneOf with and without discriminator
- Tests anyOf with overlapping properties
- Added documentation explaining semantic differences
- Demonstrates that oneOf picks first matching (untagged enum)
- Demonstrates that anyOf allows multiple matches (struct with optional fields)

The tests verify:
- oneOf generates enums (XOR semantics)
- anyOf generates structs with optional fields (OR semantics)
- oneOf with discriminator generates tagged enums
- anyOf validation ensures at least one field is set
- Added links to OpenAPI 3.1.0 spec for oneOf/anyOf definitions
- Added links to JSON Schema documentation
- Added comprehensive comparison of how different languages handle oneOf/anyOf
- Documented how Java, TypeScript, Python, Go, and C# handle these constructs
- Explained how each language deals with untagged union deserialization
- Added comparison table showing implementation approaches

Key findings:
- Most languages handle oneOf/anyOf at runtime with custom deserializers
- TypeScript uses native union types for both (no semantic difference)
- Only Rust and Python truly differentiate anyOf (OR) from oneOf (XOR)
- Java uses AbstractOpenApiSchema with TypeAdapters for both
- All implementations try types in order for untagged unions
@wing328
Copy link
Member

wing328 commented Sep 7, 2025

when it's ready, can you please pull my PR (branch) into yours to include tests for anyOf?

ref: #21911

- Documented that NO generator refuses to generate for ambiguous schemas
- Added detailed breakdown of each language's ambiguity handling strategy
- Documented Swift's oneOfUnknownDefaultCase option for unmatched values
- Explained Python's strict ValidationError approach
- Documented Java's pragmatic 'first match wins' approach
- Added common warnings and limitations from the codebase
- Included best practices for avoiding ambiguity (discriminators, mutual exclusion, ordering)
- Explained why generators choose fallbacks over refusing generation

Key findings:
- All generators have fallback strategies rather than refusing
- Python has the strictest validation (ValidationError for multiple oneOf matches)
- TypeScript is the most permissive (structural typing, no runtime validation)
- Most use 'first match wins' for untagged unions
- Pragmatism wins over strictness due to real-world API imperfections
- Added analysis of whether untagged enums violate OpenAPI/JSON Schema spec
- Documented that discriminator is optional ('when used') not required
- Clarified that oneOf MUST validate exactly one match per JSON Schema
- Documented that 'first match wins' violates strict spec compliance
- Added compliance table showing Python as only fully compliant implementation
- Included example of what fully compliant Rust code would look like

Key finding: Current implementations prioritize pragmatism over strict compliance.
Most generators (including Rust) use 'first match wins' which technically
violates the oneOf requirement that exactly one schema must match.

The spec allows validation errors, and strictly speaking, generators should
validate all schemas to ensure exactly one matches for oneOf.
- Added allOf as required composition (struct with all required fields)
- Clarified anyOf as optional composition (struct with optional fields)
- Confirmed oneOf as exclusive choice (enum)
- Added comprehensive comparison table of all three keywords
- Included correct Rust implementations for each
- Documented the fundamental pattern: composition vs choice

Key insight:
- allOf and anyOf are BOTH compositional (merge schemas)
- allOf requires ALL fields, anyOf allows optional combinations
- oneOf is NOT compositional (exclusive choice)

Most generators incorrectly treat anyOf like oneOf (choice) when it
should be compositional like allOf but with optional fields.
@timvw timvw changed the title Refactoring/rust true anyof support feat(rust): implement true anyOf support with OR semantics Sep 7, 2025
@timvw timvw marked this pull request as ready for review September 7, 2025 07:30
@wing328
Copy link
Member

wing328 commented Sep 7, 2025

anyOf: Now generates structs with optional fields (OR semantics - one or more schemas can match)

my suggestion is to focus on just this change (fix anyOf) to start with

@timvw timvw marked this pull request as draft September 7, 2025 07:45
timvw and others added 7 commits September 7, 2025 09:47
…penapi-generator into refactoring/rust-true-anyof-support
…Of support

- Merged PR OpenAPITools#21911 from wing328 which adds anyOf test samples
- Regenerated samples with our new anyOf implementation
- Models now correctly generate as structs with optional fields (OR semantics)
- ModelIdentifier and AnotherAnyOfTest demonstrate proper anyOf behavior

Co-Authored-By: William Cheng <[email protected]>
Documented the real-world implications of the anyOf fix by showing:
- How the old generator would produce duplicate enum variants (broken Rust code)
- Why wing328's test case perfectly demonstrates the problem
- The practical difference between treating anyOf as choice vs composition

These findings emerged from merging PR OpenAPITools#21911 and seeing firsthand how
the old behavior would literally generate uncompilable code for certain
anyOf schemas.
Created detailed documentation covering:
- Current type mappings from OpenAPI to Rust
- How oneOf, anyOf, and allOf are handled (or not)
- Alternative approaches that were considered
- Configuration options and their impact
- Known limitations and rationale for decisions
- Proposed changes in PR OpenAPITools#21915 for true anyOf support

This documentation provides the context needed to understand both
the current implementation and the proposed improvements.
Moved from docs/ to docs/generators/ where generator-specific
documentation belongs, alongside rust.md, rust-server.md, etc.
Enhanced the type mapping documentation with:
- Alternative composition approach for allOf using serde flatten
- Explanation of how to avoid property name conflicts
- Generated names for anonymous objects (Object$1, Object$2)
- Trade-offs between flattening vs composition approaches

This approach would maintain type safety while avoiding the complex
property merging issues that arise with allOf schemas.
Added comprehensive comparison showing how different strongly-typed
languages handle these OpenAPI constructs:

- Java: Interface/inheritance approach, treats anyOf as oneOf
- TypeScript: Union/intersection types (most natural fit)
- Go: Explicit struct with pointers and custom unmarshaling
- C#: Abstract classes and polymorphic serialization
- Python: Union types with runtime validation
- Swift: Enums with associated values (perfect for oneOf)

Key findings:
- No language correctly implements anyOf semantics (one or more)
- Most treat anyOf identical to oneOf (exactly one)
- TypeScript's type system is best suited for these constructs
- Rust's proposed struct approach is reasonable given its constraints

This comparison validates that the anyOf problem is industry-wide,
not specific to Rust, and that our proposed solution is pragmatic.
@wing328
Copy link
Member

wing328 commented Sep 9, 2025

all tests passed.

@timvw is this PR ready for review?

@timvw
Copy link
Contributor Author

timvw commented Sep 9, 2025

No... I hope to find some time to work on this coming weekend...

@wing328
Copy link
Member

wing328 commented Sep 12, 2025

FYI. There's another PR to add anyOf support to the rust-axum server generator: #21948.

Please review when you've time to see if you like the implementation in that PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants