Skip to content

Commit 73bd99d

Browse files
committed
docs(rust): add spec compliance analysis for untagged enums
- 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.
1 parent 21245d4 commit 73bd99d

File tree

1 file changed

+68
-0
lines changed

1 file changed

+68
-0
lines changed

docs/rust-oneof-anyof-semantics.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,74 @@ From the [OpenAPI 3.1.0 Specification](https://spec.openapis.org/oas/v3.1.0#sche
1616

1717
These keywords come from [JSON Schema](https://json-schema.org/understanding-json-schema/reference/combining.html) and maintain the same semantics.
1818

19+
## Should Untagged Enums Be Allowed? A Spec Analysis
20+
21+
### The Discriminator Dilemma
22+
23+
The OpenAPI specification states:
24+
25+
> "To support polymorphism, the OpenAPI Specification adds the discriminator field. When used, the discriminator will be the name of the property that decides which schema definition validates the structure of the model. As such, the discriminator field MUST be a required field."
26+
27+
However, this raises important questions:
28+
29+
1. **Is discriminator required for all oneOf schemas?** No, the spec says "when used" - it's optional.
30+
2. **Does oneOf without discriminator violate the spec?** No, but it may violate the intent.
31+
32+
### JSON Schema vs OpenAPI Semantics
33+
34+
**JSON Schema requirement** (which OpenAPI inherits):
35+
- oneOf: "The given data must be valid against **exactly one** of the given subschemas"
36+
- This requires checking ALL subschemas to ensure only one matches
37+
38+
**Implementation reality**:
39+
- Most generators use "first match wins" for untagged unions
40+
- This violates the strict oneOf semantics unless additional validation is performed
41+
42+
### The Case for Validation Errors
43+
44+
**You're correct that strictly speaking, generators should validate that exactly one schema matches for oneOf.** This means:
45+
46+
1. **Untagged enums are technically non-compliant** if they don't validate exclusivity
47+
2. **Validation errors should be thrown** when multiple schemas match
48+
3. **"First match wins" is a pragmatic compromise** that violates the spec
49+
50+
### Current Implementations vs Spec Compliance
51+
52+
| Approach | Spec Compliant? | Used By |
53+
|----------|----------------|---------|
54+
| First match wins (no validation) | ❌ No | Rust, Java, C# |
55+
| Validate exactly one matches | ✅ Yes | Python (Pydantic) |
56+
| Require discriminator | ✅ Yes (conservative) | None (but recommended) |
57+
| Generate error for ambiguous schemas | ✅ Yes (conservative) | None currently |
58+
59+
### Implications for Rust Implementation
60+
61+
The current Rust implementation using untagged enums is **pragmatic but not strictly compliant** because:
62+
63+
1. Serde's `#[serde(untagged)]` stops at first match
64+
2. No validation that other variants wouldn't also match
65+
3. Could silently accept invalid data that matches multiple schemas
66+
67+
**To be fully compliant**, Rust would need to:
68+
```rust
69+
// Validate against all variants
70+
impl<'de> Deserialize<'de> for OneOfExample {
71+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> {
72+
let value = Value::deserialize(deserializer)?;
73+
let mut matches = 0;
74+
75+
if let Ok(_) = Type1::deserialize(&value) { matches += 1; }
76+
if let Ok(_) = Type2::deserialize(&value) { matches += 1; }
77+
78+
if matches != 1 {
79+
return Err(Error::custom("Must match exactly one schema"));
80+
}
81+
82+
// Then do actual deserialization
83+
}
84+
}
85+
```
86+
1987
## Implementation Details
2088

2189
### oneOf - Untagged Enums

0 commit comments

Comments
 (0)