Unions and polymorphism
Typra supports closed string unions, open string unions, and discriminated model hierarchies.
Closed string unions
Section titled “Closed string unions”@parseAlias("ready", #["complete"])union FixtureStatus { draft: "draft"; ready: "ready"; archived: "archived";}@parseAlias is parse-only. Loading accepts complete, but saving emits the canonical TypeSpec value ready.
String literal aliases keep their declared name when combined with null.
For alias Mood = "calm" | "busy";, a field mood: Mood | null uses the generated
type Mood, not mood. Regenerating code produced by 3.0.0 can change these
property-derived type names; update any direct references to the old names.
Go and Swift declare an enum shared by several models once in their package or module. Rust also declares it once and imports it into the other model files. Use Rust’s generated module re-exports rather than depending on which model file owns the enum.
Open string unions
Section titled “Open string unions”@parseAlias("batch", #["bulk"])union FixtureMode { interactive: "interactive"; batch: "batch"; custom: string;}Open unions preserve known values while allowing target-specific handling for custom strings.
Discriminated models
Section titled “Discriminated models”@discriminator("kind")model FixtureContent { kind: string;}
model TextContent extends FixtureContent { kind: "text"; text: string;}
model ImageContent extends FixtureContent { kind: "image"; url: string;}Typra uses the discriminator field to load and save the right child shape. In the fixture contract, FixtureRoot.content and FixtureRoot.contentItems exercise both a single polymorphic value and a polymorphic collection.
Rust stores a single polymorphic reference as raw serde_json::Value. Saving its
parent preserves that JSON without filling in nested defaults, including absent
T | null = null fields. Direct model loading and typed polymorphic collections
do apply those defaults. This limitation is unchanged.
Generated shape
Section titled “Generated shape”export type FixtureMode = "interactive" | "batch" | (string & {});export type FixtureStatus = "draft" | "ready" | "archived";
function parseFixtureStatus(value: unknown): FixtureStatus { switch (String(value)) { case "complete": return "ready"; default: throw new Error(`Invalid FixtureStatus value: ${value}`); }}
private static loadKind(data: Record<string, unknown>, context?: LoadContext): FixtureContent { switch (String(data["kind"]).toLowerCase()) { case "text": return TextContent.load(data, context); case "image": return ImageContent.load(data, context); default: return new FixtureContent(); }}FixtureMode = Literal["interactive", "batch"] | strFixtureStatus = Literal["draft", "ready", "archived"]
def _parse_fixture_status(value: Any) -> FixtureStatus: aliases = {"draft": "draft", "ready": "ready", "archived": "archived", "complete": "ready"} if str(value) in aliases: return aliases[str(value)] raise ValueError(f"Invalid FixtureStatus value: {value}")
class FixtureContent: @staticmethod def load(data: Any, context: LoadContext | None = None) -> "FixtureContent": if data.get("kind") == "text": return TextContent.load(data, context) if data.get("kind") == "image": return ImageContent.load(data, context) return FixtureContent()public enum FixtureStatus{ Draft, Ready, Archived}
public static class FixtureStatusParser{ public static FixtureStatus Parse(string value) => value switch { "complete" => FixtureStatus.Ready, _ => Enum.Parse<FixtureStatus>(value, true) };}
return discriminator switch{ "text" => TextContent.Load(data, context), "image" => ImageContent.Load(data, context), _ => new FixtureContent(),};func ParseFixtureStatus(value string) (FixtureStatus, error) { switch value { case "ready", "complete": return FixtureStatusReady, nil default: return "", fmt.Errorf("invalid FixtureStatus value: %s", value) }}
func LoadFixtureContent(data interface{}, ctx *LoadContext) (interface{}, error) { if m, ok := data.(map[string]interface{}); ok { switch m["kind"] { case "text": return LoadTextContent(data, ctx) case "image": return LoadImageContent(data, ctx) } } return FixtureContent{}, nil}enum FixtureStatus { DRAFT("draft"), READY("ready"), ARCHIVED("archived");
public static FixtureStatus fromValue(String value) { if (value.equals("complete")) return READY; for (FixtureStatus item : values()) { if (item.value.equals(value)) return item; } return valueOf(value.toUpperCase().replace("-", "_")); }}
if ("text".equals(map.get("kind"))) return TextContent.load(input, ctx);if ("image".equals(map.get("kind"))) return ImageContent.load(input, ctx);pub enum FixtureMode { Interactive, Batch, Other(String),}
impl FixtureStatus { pub fn from_str_ignore_case_opt(s: &str) -> Option<Self> { if s.eq_ignore_ascii_case("complete") { return Some(Self::Ready); } None }}
pub enum FixtureContent { TextContent(TextContent), ImageContent(ImageContent),}public enum FixtureStatus: String, Codable, CaseIterable { case draft, ready, archived public static func parse(_ value: String) throws -> FixtureStatus { switch value { case "draft": return .draft case "ready", "complete": return .ready case "archived": return .archived default: throw TypraRuntimeError.invalidEnum(type: "FixtureStatus", value: value) } }}
public struct FixtureMode: RawRepresentable, Equatable, Hashable, Codable { public let rawValue: String public static let interactive = FixtureMode(rawValue: "interactive") public static let batch = FixtureMode(rawValue: "batch") public static func parse(_ value: String) throws -> FixtureMode { switch value { case "bulk": return .batch default: return FixtureMode(rawValue: value) } }}
public enum FixtureContent: TypraModel { case textContent(TextContent) case imageContent(ImageContent) case unknown([String: Any])
public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FixtureContent { let object = try TypraRuntime.object(data, typeName: "FixtureContent") switch try TypraRuntime.string(object["kind"] ?? "", field: "kind") { case "text": return .textContent(try TextContent.load(data, context: context)) case "image": return .imageContent(try ImageContent.load(data, context: context)) default: return .unknown(object) } }}{ "unions": { "FixtureStatus": { "values": ["draft", "ready", "archived"], "aliases": { "complete": "ready" } }, "FixtureMode": { "values": ["interactive", "batch"], "open": true, "aliases": { "bulk": "batch" } } }, "discriminators": { "FixtureContent": { "property": "kind", "children": { "text": "TextContent", "image": "ImageContent" } } }}