Skip to content

Commit ffe54b5

Browse files
committed
docs: add cross-language comparison for oneOf/anyOf/allOf handling
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.
1 parent 86ff272 commit ffe54b5

File tree

1 file changed

+247
-1
lines changed

1 file changed

+247
-1
lines changed

docs/generators/rust-type-mapping.md

Lines changed: 247 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,252 @@ if let Some(n) = &model_identifier.as_any_of_1 {
515515
4. **Phase 4**: Add discriminator support for anyOf
516516
5. **Phase 5**: Consider union type alternatives when Rust supports them
517517

518+
## Cross-Language Comparison: How Other Strongly-Typed Languages Handle oneOf/anyOf/allOf
519+
520+
### Java
521+
522+
**oneOf**:
523+
- Generates an interface/abstract class with concrete implementations
524+
- With discriminator: Uses Jackson's `@JsonTypeInfo` and `@JsonSubTypes` for polymorphic deserialization
525+
- Without discriminator: Creates wrapper class with multiple typed fields, only one can be set
526+
- Recent versions support sealed interfaces (Java 17+)
527+
528+
```java
529+
// oneOf with discriminator
530+
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "petType")
531+
@JsonSubTypes({
532+
@JsonSubTypes.Type(value = Cat.class, name = "cat"),
533+
@JsonSubTypes.Type(value = Dog.class, name = "dog")
534+
})
535+
public abstract class Pet { }
536+
537+
// oneOf without discriminator
538+
public class StringOrNumber {
539+
private String stringValue;
540+
private Integer numberValue;
541+
542+
// Only one setter can be called
543+
public void setString(String value) {
544+
this.stringValue = value;
545+
this.numberValue = null;
546+
}
547+
}
548+
```
549+
550+
**anyOf**:
551+
- Often treated same as oneOf (incorrect)
552+
- Some implementations generate a class with all possible fields as nullable
553+
- No true validation that at least one matches
554+
555+
**allOf**:
556+
- Uses inheritance when possible (extends base class)
557+
- Composition with interfaces for multiple inheritance
558+
- Flattens properties into single class
559+
560+
### TypeScript
561+
562+
**oneOf/anyOf**:
563+
- Uses union types naturally: `type Pet = Cat | Dog`
564+
- Runtime validation required (not enforced by type system)
565+
- anyOf and oneOf generate identical code (union types)
566+
567+
```typescript
568+
// Both oneOf and anyOf generate:
569+
export type StringOrNumber = string | number;
570+
export type Pet = Cat | Dog;
571+
572+
// With discriminator:
573+
export type Pet =
574+
| { petType: "cat" } & Cat
575+
| { petType: "dog" } & Dog;
576+
```
577+
578+
**allOf**:
579+
- Uses intersection types: `type Employee = Person & { employeeId: string }`
580+
- Natural composition support in type system
581+
582+
```typescript
583+
export type Employee = Person & {
584+
employeeId: string;
585+
department: string;
586+
};
587+
```
588+
589+
### Go
590+
591+
**oneOf/anyOf**:
592+
- No union types in Go
593+
- Generates struct with pointers to all possible types
594+
- Custom marshaling/unmarshaling logic
595+
- Validation methods to ensure constraints
596+
597+
```go
598+
// oneOf/anyOf implementation
599+
type StringOrNumber struct {
600+
String *string `json:"-"`
601+
Number *float32 `json:"-"`
602+
}
603+
604+
func (s *StringOrNumber) UnmarshalJSON(data []byte) error {
605+
// Try unmarshaling as each type
606+
var str string
607+
if err := json.Unmarshal(data, &str); err == nil {
608+
s.String = &str
609+
return nil
610+
}
611+
612+
var num float32
613+
if err := json.Unmarshal(data, &num); err == nil {
614+
s.Number = &num
615+
return nil
616+
}
617+
618+
return errors.New("could not unmarshal as string or number")
619+
}
620+
```
621+
622+
**allOf**:
623+
- Uses struct embedding (composition)
624+
- Flattens nested structures
625+
- Can have field name conflicts
626+
627+
```go
628+
type Employee struct {
629+
Person // Embedded struct
630+
EmployeeId string `json:"employeeId"`
631+
Department string `json:"department"`
632+
}
633+
```
634+
635+
### C# (.NET)
636+
637+
**oneOf**:
638+
- Generates abstract base class with derived types
639+
- With discriminator: Uses JsonConverter for polymorphic serialization
640+
- Without discriminator: Wrapper class with nullable properties
641+
642+
```csharp
643+
// oneOf with discriminator
644+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "petType")]
645+
[JsonDerivedType(typeof(Cat), "cat")]
646+
[JsonDerivedType(typeof(Dog), "dog")]
647+
public abstract class Pet { }
648+
649+
// oneOf without discriminator
650+
public class StringOrNumber
651+
{
652+
public string? AsString { get; set; }
653+
public int? AsNumber { get; set; }
654+
655+
// Validation ensures only one is set
656+
}
657+
```
658+
659+
**anyOf**:
660+
- Similar to oneOf but allows multiple properties to be set
661+
- Often incorrectly treated as oneOf
662+
663+
**allOf**:
664+
- Uses inheritance for single parent
665+
- Composition pattern for multiple schemas
666+
- Properties flattened into derived class
667+
668+
### Python (with Pydantic)
669+
670+
**oneOf/anyOf**:
671+
- Uses `Union` type hint
672+
- Pydantic validates at runtime
673+
- First matching schema wins (for both oneOf and anyOf)
674+
675+
```python
676+
# oneOf/anyOf
677+
StringOrNumber = Union[str, int]
678+
679+
class Pet(BaseModel):
680+
__root__: Union[Cat, Dog]
681+
682+
# With discriminator
683+
class Config:
684+
discriminator = 'petType'
685+
```
686+
687+
**allOf**:
688+
- Multiple inheritance with mixins
689+
- Pydantic handles field merging
690+
691+
```python
692+
class Employee(Person, EmployeeFields):
693+
pass # Inherits from both
694+
```
695+
696+
### Swift
697+
698+
**oneOf**:
699+
- Native enum with associated values (perfect fit!)
700+
- Type-safe and ergonomic
701+
702+
```swift
703+
enum Pet: Codable {
704+
case cat(Cat)
705+
case dog(Dog)
706+
707+
// Custom coding keys for discriminator
708+
}
709+
710+
enum StringOrNumber: Codable {
711+
case string(String)
712+
case number(Int)
713+
}
714+
```
715+
716+
**anyOf**:
717+
- No native support
718+
- Usually treated as oneOf (enum)
719+
720+
**allOf**:
721+
- Protocol composition
722+
- Struct with all properties
723+
724+
### Comparison Table
725+
726+
| Language | oneOf | anyOf | allOf |
727+
|----------|-------|-------|-------|
728+
| **Rust (current)** | Enum (untagged) | Enum (incorrect) | ❌ Not supported |
729+
| **Rust (proposed)** | Enum (untagged) | Struct with Option fields | ❌ Not supported |
730+
| **Java** | Interface + implementations | Same as oneOf (incorrect) | Inheritance/Composition |
731+
| **TypeScript** | Union type | Union type (same) | Intersection type |
732+
| **Go** | Struct with pointers | Struct with pointers | Struct embedding |
733+
| **C#** | Abstract class + derived | Similar to oneOf | Inheritance/Composition |
734+
| **Python** | Union + validation | Union (same) | Multiple inheritance |
735+
| **Swift** | Enum with associated values | Enum (incorrect) | Protocol composition |
736+
737+
### Key Observations
738+
739+
1. **No language perfectly handles anyOf**: Most treat it identical to oneOf, missing the "one or more" semantics
740+
2. **TypeScript has the best model**: Union and intersection types naturally express these concepts
741+
3. **Swift enums are ideal for oneOf**: Associated values provide perfect type safety
742+
4. **Go's approach is explicit**: No magic, clear what's happening, verbose but correct
743+
5. **Dynamic languages rely on runtime validation**: Python/Ruby validate at runtime, not compile time
744+
745+
### Why Rust's Proposed Approach Makes Sense
746+
747+
Given Rust's type system constraints:
748+
- **No union types**: Can't do TypeScript-style unions
749+
- **No inheritance**: Can't do Java/C# style class hierarchies
750+
- **Strong type safety**: Want compile-time guarantees
751+
752+
The proposed struct-with-optional-fields for anyOf:
753+
- **Explicit about semantics**: Clear that multiple can be set
754+
- **Type safe**: Compiler enforces field types
755+
- **Serde compatible**: Works with existing serialization
756+
- **Migration path**: Different from enum, so existing code breaks loudly (good!)
757+
518758
## Conclusion
519759

520-
The current Rust generator makes pragmatic choices favoring simplicity and Rust idioms over strict OpenAPI compliance. The proposed changes in PR #21915 move toward semantic correctness while maintaining reasonable ergonomics. Future work should focus on incremental improvements guided by user needs and Rust ecosystem evolution.
760+
The current Rust generator makes pragmatic choices favoring simplicity and Rust idioms over strict OpenAPI compliance. The proposed changes in PR #21915 move toward semantic correctness while maintaining reasonable ergonomics.
761+
762+
Comparing with other languages shows that:
763+
1. No language has solved anyOf perfectly
764+
2. Rust's constraints (no unions, no inheritance) require creative solutions
765+
3. The proposed struct approach for anyOf is reasonable given these constraints
766+
4. Future work should focus on incremental improvements guided by user needs and Rust ecosystem evolution

0 commit comments

Comments
 (0)