Collections and records
Typra preserves the collection shape from TypeSpec and lets each target choose the idiomatic runtime type.
model FixtureRoot { tags: string[]; metadata?: Record<unknown>; contentItems: FixtureContent[];}| TypeSpec | Typra mapping |
|---|---|
string[] |
Array or list of scalar values. |
FixtureContent[] |
Array or list of nested model values, including polymorphic dispatch when the item type is discriminated. |
Record<unknown> |
Dictionary-shaped metadata with unknown values. |
Record<string> |
Dictionary with string values. |
Generated tests exercise scalar arrays, nested object arrays, dictionaries, and polymorphic collections from fixture samples.
In C#, Record<string> emits IDictionary<string, string>. Loading a null entry
throws an error that includes its key path. Record<string> | null = null allows
the whole dictionary to be absent or null, but its entries still cannot be null.
Use Record<unknown> for values that may include null.
Unknown-field passthrough
Section titled “Unknown-field passthrough”A model whose string indexer is unknown round-trips fields that it does not declare, so a document
contract can carry keys that older clients do not model. Spread ...Record<unknown> into the model, or
declare it with is Record<unknown> or extends Record<unknown>:
model PlanningRow { time: string;
@encodedName("application/json", "started_at") startedAt?: string;
...Record<unknown>;}Every generated model gets a public extras member typed as the runtime’s dynamic JSON map, defaulting to empty:
| Target | Member |
|---|---|
| TypeScript | additionalProperties: Record<string, unknown> (zod schemas add .passthrough()) |
| Python | additional_properties: dict[str, Any] |
| Go | AdditionalProperties map[string]interface{} (tagged json:"-" yaml:"-") |
| C# | Dictionary<string, object?> AdditionalProperties |
| Java | Map<String, Object> additionalProperties (@JsonIgnore under Jackson) |
| Rust | additional_properties: serde_json::Map<String, serde_json::Value> |
| Swift | additionalProperties: [String: Any] |
Load puts every input key that is not a declared wire key into the member. Known keys are matched by wire
name, so started_at above is a declared field, and they include inherited fields and the discriminator.
Save writes the declared fields first, then each extras entry whose key is not a declared wire key, so a
declared field always wins and no key is written twice. A model derived from a passthrough base passes
through as well, using its own known keys.
Only Record<unknown> is supported. A typed indexer such as ...Record<string> reports the
typra-emitter-unsupported-additional-properties warning and its extra keys are dropped. Passthrough models
also get a generated known-key helper (isKnownWireKey in TypeScript and Java, IsKnownWireKey in C#,
_known_wire_keys in Python, additionalPropertiesKnownKeys in Swift). A passthrough model that declares, or
inherits, a property that any target would spell like the extras member or one of these helpers (for example
additionalProperties, additional_properties, or isKnownWireKey) reports the
typra-emitter-additional-properties-collision error; rename the property.
In TypeScript the extras member is a null-prototype object (Object.create(null)) and save writes each entry
with Object.defineProperty, so an unknown __proto__ key round-trips as ordinary data and never changes an
object’s prototype.
Generated shape
Section titled “Generated shape”export class FixtureRoot { tags: string[] = []; metadata?: Record<string, unknown> | undefined; contentItems: FixtureContent[] = [];
static loadContentItems(data: Record<string, unknown>[] | unknown[], context?: LoadContext): FixtureContent[] { return data.map(item => FixtureContent.load(item as Record<string, unknown>, context)); }
static saveContentItems(items: FixtureContent[], context?: SaveContext): Record<string, unknown>[] { return items.map(item => item.save(context)); }}@dataclassclass FixtureRoot: tags: list[str] = field(default_factory=list) metadata: dict[str, Any] | None = None content_items: list[FixtureContent] = field(default_factory=list)
@staticmethod def load_content_items(data: dict | list, context: LoadContext | None) -> list[FixtureContent]: if isinstance(data, dict): data = [{"name": k, **v} if isinstance(v, dict) else {"name": k, "kind": v} for k, v in data.items()] return [FixtureContent.load(item, context) for item in data]public partial class FixtureRoot{ public IList<string> Tags { get; set; } = []; public IDictionary<string, object>? Metadata { get; set; } public IList<FixtureContent> ContentItems { get; set; } = [];
public static IList<FixtureContent> LoadContentItems(object data, LoadContext? context) { var result = new List<FixtureContent>(); foreach (var item in (IEnumerable<object>)data) result.Add(FixtureContent.Load(item.GetDictionary(FixtureContent.ShorthandProperty), context)); return result; }}type FixtureRoot struct { Tags []string `json:"tags" yaml:"tags"` Metadata map[string]interface{} `json:"metadata,omitempty" yaml:"metadata,omitempty"` ContentItems []interface{} `json:"contentItems" yaml:"contentItems"`}
for i, v := range arr { if item, ok := v.(map[string]interface{}); ok { loaded, _ := LoadFixtureContent(item, ctx) result.ContentItems[i] = loaded }}public class FixtureRoot { public List<String> tags = new ArrayList<>(); public Map<String, Object> metadata = null; public List<FixtureContent> contentItems = new ArrayList<>();
if (map.get("contentItems") instanceof Iterable<?> values) { for (Object item : values) { result.contentItems.add(FixtureContent.load(item, ctx)); } }}pub struct FixtureRoot { pub tags: Vec<String>, pub metadata: serde_json::Value, pub content_items: Vec<FixtureContent>,}
content_items: value .get("contentItems") .map(|v| Self::load_content_items(v, ctx)) .unwrap_or_default(),public struct FixtureRoot: TypraModel { public var tags: [String] = [] public var metadata: [String: Any]? = nil public var contentItems: [FixtureContent] = []
public static func load(_ data: Any, context: LoadContext = LoadContext()) throws -> FixtureRoot { let object = try TypraRuntime.object(data, typeName: "FixtureRoot") var instance = FixtureRoot() if let value = object["tags"] { instance.tags = try TypraRuntime.array(value, field: "tags").map { try TypraRuntime.string($0, field: "tags") } } if let value = object["metadata"] { instance.metadata = try TypraRuntime.dictionary(value, field: "metadata") } if let value = object["contentItems"] { instance.contentItems = try TypraRuntime.array(value, field: "contentItems").map { try FixtureContent.load($0, context: context) } } return instance }}{ "properties": { "tags": { "type": "array", "items": "string" }, "metadata": { "type": "record", "value": "unknown", "optional": true }, "contentItems": { "type": "array", "items": "FixtureContent" } }}Review note
Section titled “Review note”Collections are often where target-specific ergonomics differ. Review the generated target page for the runtime you intend to publish, especially around nullability, unknown values, and dictionary value types.