Skip to main content

Polymorphic Types

ModelGraphGenerator has first-class support for polymorphic properties — fields that can hold one of several concrete types at runtime, discriminated by a key in the serialized JSON.

The Problem

A plain Swift protocol property gives no information about which concrete types can appear at runtime:

protocol Payment {}

struct Order {
let payment: Payment // ← could be anything
}

Without additional information, the JSON Schema for payment would just be {} — useless for validation.

The Solution — @PolymorphicMapping

Annotate the property with the concrete types and a discriminator key:

struct Order {
@PolymorphicMapping(
discriminator: "type",
variants: [
"credit": CreditPayment.self,
"upi": UPIPayment.self,
"wallet": WalletPayment.self,
]
)
let payment: Payment
}

ModelGraphGenerator reads this annotation and generates a oneOf schema with a discriminator block.

Processing Flow

Generated JSON Schema

"payment": {
"oneOf": [
{ "$ref": "#/$defs/CreditPayment" },
{ "$ref": "#/$defs/UPIPayment" },
{ "$ref": "#/$defs/WalletPayment" }
],
"discriminator": {
"propertyName": "type",
"mapping": {
"credit": "#/$defs/CreditPayment",
"upi": "#/$defs/UPIPayment",
"wallet": "#/$defs/WalletPayment"
}
}
}

Each variant type is also fully expanded in $defs:

"$defs": {
"CreditPayment": {
"type": "object",
"properties": {
"type": { "type": "string", "const": "credit" },
"cardNumber": { "type": "string" },
"expiry": { "type": "string" }
},
"required": ["type", "cardNumber", "expiry"]
}
}

Protocol Conformance Detection

As an alternative to explicit @PolymorphicMapping, ModelGraphGenerator can also detect polymorphism through protocol conformance when --protocol-name is used. In this mode:

findSymbolsConforming(to: "Payment")
→ [CreditPayment, UPIPayment, WalletPayment]

However without a discriminator key in the source, the discriminator block in the output will be absent — the schema validator won't know which key to switch on. Using @PolymorphicMapping is always more precise.

Depth Handling

Polymorphic variants are expanded at isPolymorphic: true recursion frames. The depth counter for variants starts from the parent property's depth, not from zero, ensuring that polymorphic types deep inside a model are still bounded by the global depth limit.

Nested Polymorphism

A variant type may itself have polymorphic properties:

struct CreditPayment {
@PolymorphicMapping(
discriminator: "issuer",
variants: ["visa": VisaCard.self, "mc": MasterCard.self]
)
let card: Card
}

This is handled naturally — processSymbol(CreditPayment) recursively calls the same polymorphic expansion logic on CreditPayment's properties. The cycle detector prevents any loops.

Summary

FeatureSupported
@PolymorphicMapping annotation
oneOf + discriminator in output
Variant types fully expanded in $defs
Protocol conformance detection
Nested polymorphism
Discriminator const in variant schema
Cycles within variant graphs