|
| 1 | +# Data Model: Recursive Type Schema Generation |
| 2 | + |
| 3 | +**Feature**: 003-recursive-types |
| 4 | +**Date**: 2026-02-06 |
| 5 | + |
| 6 | +## Entities |
| 7 | + |
| 8 | +### SchemaNode (Existing - No Changes) |
| 9 | + |
| 10 | +The core intermediate representation for JSON Schema nodes. Already includes the `Ref` variant that handles recursive references. |
| 11 | + |
| 12 | +| Variant | Description | Recursive Relevance | |
| 13 | +|---------|-------------|-------------------| |
| 14 | +| `Ref of typeId: string` | Reference to another schema definition | `"#"` = self-reference to root; other strings = definition reference | |
| 15 | +| `AnyOf of SchemaNode list` | Union of schemas (DU representation) | Root DU produces AnyOf of Refs to case definitions | |
| 16 | +| `Object` | Object with properties | DU cases and records produce Object nodes | |
| 17 | +| `Array of SchemaNode` | Array with item schema | Items can be `Ref` for recursive collections | |
| 18 | +| `Nullable of SchemaNode` | Nullable wrapper | Inner schema can be `Ref` for recursive option fields | |
| 19 | +| Other variants | Primitive, Enum, Map, Const, OneOf, Any | Not directly involved in recursion | |
| 20 | + |
| 21 | +### SchemaDocument (Existing - No Changes) |
| 22 | + |
| 23 | +``` |
| 24 | +SchemaDocument = { |
| 25 | + Root: SchemaNode -- Top-level schema (AnyOf for DUs, Object for records) |
| 26 | + Definitions: (string * SchemaNode) list -- Named definitions in insertion order |
| 27 | +} |
| 28 | +``` |
| 29 | + |
| 30 | +### Recursion Detection State (Internal - No Changes) |
| 31 | + |
| 32 | +| State | Type | Purpose | |
| 33 | +|-------|------|---------| |
| 34 | +| `visiting` | `HashSet<Type>` | Tracks types currently being analyzed (cycle detection) | |
| 35 | +| `analyzed` | `Dictionary<Type, string>` | Caches completed type-to-typeId mappings | |
| 36 | +| `definitions` | `Dictionary<string, SchemaNode>` | Accumulates definitions in insertion order | |
| 37 | + |
| 38 | +## Recursive Type Patterns |
| 39 | + |
| 40 | +### Pattern 1: Self-Recursive DU (Issue #15) |
| 41 | + |
| 42 | +**F# Type**: |
| 43 | +```fsharp |
| 44 | +type TreeNode = |
| 45 | + | Leaf of int |
| 46 | + | Branch of TreeNode * TreeNode |
| 47 | +``` |
| 48 | + |
| 49 | +**Expected SchemaDocument**: |
| 50 | +``` |
| 51 | +Root = AnyOf [Ref "Leaf"; Ref "Branch"] |
| 52 | +Definitions = [ |
| 53 | + ("Leaf", Object { Properties = [kind="Leaf" (Const); Item (Primitive Int)] }) |
| 54 | + ("Branch", Object { Properties = [kind="Branch" (Const); Item1 (Ref "#"); Item2 (Ref "#")] }) |
| 55 | +] |
| 56 | +``` |
| 57 | + |
| 58 | +### Pattern 2: Self-Recursive Record |
| 59 | + |
| 60 | +**F# Type**: |
| 61 | +```fsharp |
| 62 | +type LinkedNode = { Value: int; Next: LinkedNode option } |
| 63 | +``` |
| 64 | + |
| 65 | +**Expected SchemaDocument**: |
| 66 | +``` |
| 67 | +Root = Object { Properties = [value (Primitive Int); next (Nullable (Ref "#"))] } |
| 68 | +Definitions = [] |
| 69 | +``` |
| 70 | + |
| 71 | +### Pattern 3: Recursion Through Collection |
| 72 | + |
| 73 | +**F# Type**: |
| 74 | +```fsharp |
| 75 | +type TreeRecord = { Value: string; Children: TreeRecord list } |
| 76 | +``` |
| 77 | + |
| 78 | +**Expected SchemaDocument**: |
| 79 | +``` |
| 80 | +Root = Object { Properties = [value (Primitive String); children (Array (Ref "#"))] } |
| 81 | +Definitions = [] |
| 82 | +``` |
| 83 | + |
| 84 | +### Pattern 4: Multi-Case Self-Recursive DU |
| 85 | + |
| 86 | +**F# Type**: |
| 87 | +```fsharp |
| 88 | +type Expression = |
| 89 | + | Literal of int |
| 90 | + | Add of Expression * Expression |
| 91 | + | Negate of Expression |
| 92 | +``` |
| 93 | + |
| 94 | +**Expected SchemaDocument**: |
| 95 | +``` |
| 96 | +Root = AnyOf [Ref "Literal"; Ref "Add"; Ref "Negate"] |
| 97 | +Definitions = [ |
| 98 | + ("Literal", Object { Properties = [kind="Literal" (Const); Item (Primitive Int)] }) |
| 99 | + ("Add", Object { Properties = [kind="Add" (Const); Item1 (Ref "#"); Item2 (Ref "#")] }) |
| 100 | + ("Negate", Object { Properties = [kind="Negate" (Const); Item (Ref "#")] }) |
| 101 | +] |
| 102 | +``` |
| 103 | + |
| 104 | +## Relationships |
| 105 | + |
| 106 | +``` |
| 107 | +SchemaAnalyzer.analyze |
| 108 | + ├── analyzeType → analyzeMultiCaseDU (for DU types) |
| 109 | + │ ├── buildCaseSchema → analyzeDuCaseFieldSchema |
| 110 | + │ │ └── getOrAnalyzeRef → detects visiting set → Ref "#" or Ref typeId |
| 111 | + │ └── definitions accumulate case schemas |
| 112 | + ├── analyzeType → analyzeRecord (for record types) |
| 113 | + │ └── analyzeFieldSchema → getOrAnalyzeRef → Ref "#" or Ref typeId |
| 114 | + └── Returns SchemaDocument { Root; Definitions } |
| 115 | +``` |
| 116 | + |
| 117 | +## State Transitions |
| 118 | + |
| 119 | +No state transitions apply - schema generation is a pure analysis pass that reads F# type metadata and produces an immutable SchemaDocument. |
0 commit comments