Skip to content

feat: add support for recursive type schema generation#28

Merged
panesofglass merged 2 commits intomasterfrom
003-recursive-types
Feb 7, 2026
Merged

feat: add support for recursive type schema generation#28
panesofglass merged 2 commits intomasterfrom
003-recursive-types

Conversation

@panesofglass
Copy link
Collaborator

Summary

Implements support for generating JSON schemas from self-recursive F# types, resolving GitHub issue #15. This adds the ability to handle discriminated unions, records, and complex recursive structures that reference themselves.

Key Changes

Bug Fix: NJsonSchemaTranslator Self-Reference Handling

  • Problem: The translator failed to serialize schemas for types with nullable self-references (e.g., LinkedNode with Next: LinkedNode option)
  • Error: "Could not find the JSON path of a referenced schema: Manually referenced schemas must be added to the 'Definitions' of a parent schema"
  • Root Cause: Nullable Ref handlers tried to look up "#" in the definitions dictionary instead of referencing rootSchema
  • Solution: Added explicit SchemaNode.Ref "#" match cases in both translateProp and translateNode Nullable branches

Implementation Details

  • Modified: src/FSharp.Data.JsonSchema.NJsonSchema/NJsonSchemaTranslator.fs
    • Added self-reference handling in translateProp (lines 107-119)
    • Added self-reference handling in translateNode (lines 180-187)
  • Added comprehensive tests for 4 recursive type patterns:
    • Self-recursive discriminated unions (TreeNode)
    • Self-recursive records with optional fields (LinkedNode)
    • Recursion through collections (TreeRecord)
    • Multi-case recursive discriminated unions (Expression)

Test Coverage

All 573 tests passing across all frameworks:

  • Core Tests: 51 × 3 frameworks = 153 ✓
  • Main NJsonSchema Tests: 128 × 3 frameworks = 384 ✓
  • OpenApi Integration Tests: 18 × 2 frameworks = 36 ✓

Recursive Type Examples

Self-Recursive DU:

type TreeNode = | Leaf of int | Branch of TreeNode * TreeNode

Generated schema uses $ref: "#" for circular references in Branch cases.

Self-Recursive Record with Optional:

type LinkedNode = { Value: int; Next: LinkedNode option }

Generated schema correctly handles nullable self-reference as oneOf: [null, $ref: "#"].

Recursion Through Collections:

type TreeRecord = { Value: string; Children: TreeRecord list }

Generated schema allows array items to reference root via $ref: "#".

Multi-Case Recursive DU:

type Expression = | Literal of int | Add of Expression * Expression | Negate of Expression

Generated schema handles multiple recursive cases with proper definition references.

Verified Snapshots

  • GeneratorTests.Self-recursive DU generates proper schema.verified.txt
  • GeneratorTests.Self-recursive record generates proper schema.verified.txt
  • GeneratorTests.Recursion through collection generates proper schema.verified.txt
  • GeneratorTests.Multi-case self-recursive DU generates proper schema.verified.txt

Testing Strategy

The implementation passes all existing tests plus new comprehensive test coverage:

  • Core IR tests verify proper schema document structure
  • Snapshot tests verify JSON serialization matches expected format
  • Validation tests verify generated schemas accept valid recursive instances
  • OpenApi integration tests verify transformer compatibility

Test Plan

  • All Core tests pass (51 × 3 frameworks)
  • All Main tests pass (128 × 3 frameworks)
  • All OpenApi tests pass (18 × 2 frameworks)
  • Recursive type schemas serialize correctly
  • Validation tests accept recursive instances
  • Snapshots match across all frameworks

🤖 Generated with Claude Code

Fixed critical bug where nullable self-references (Ref "#") were not properly
translated. When a type has a field with nullable self-reference (e.g.,
LinkedNode with Next: LinkedNode option), the translator was trying to look up
"#" in the definitions dictionary instead of referencing rootSchema directly.

Added special case handling for SchemaNode.Ref "#" in both translateProp and
translateNode functions' Nullable match branches. This allows schemas like
LinkedNode to correctly serialize to JSON with proper self-reference structure.

All 573 tests now pass across all frameworks:
- Core tests: 51 × 3 frameworks
- Main tests: 128 × 3 frameworks
- OpenApi tests: 18 × 2 frameworks

Fixes GitHub issue #15 for self-recursive type support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Base automatically changed from 002-extended-types to master February 7, 2026 18:27
@panesofglass panesofglass merged commit e28a4ff into master Feb 7, 2026
1 check passed
@panesofglass panesofglass deleted the 003-recursive-types branch February 7, 2026 18:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant