Skip to content

Commit b506d2d

Browse files
authored
tool: add recursive structure support in JSON schema (#447)
Implement $ref and $defs in schema generator to handle recursive and mutually recursive data structures. https://json-schema.org/draft/2020-12/json-schema-core#name-schema-re-use-with-defs
1 parent 0e0d3ec commit b506d2d

File tree

4 files changed

+542
-70
lines changed

4 files changed

+542
-70
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package tool_test
2+
3+
import (
4+
"encoding/json"
5+
"reflect"
6+
"testing"
7+
8+
itool "trpc.group/trpc-go/trpc-agent-go/internal/tool"
9+
)
10+
11+
// Pet defines the user's fury friend.
12+
type Pet struct {
13+
// Name of the animal.
14+
Name string `json:"name" jsonschema:"title=Name"`
15+
// Animal type, e.g., dog, cat, hamster.
16+
AnimalType AnimalType `json:"animal_type" jsonschema:"title=Animal Type"`
17+
}
18+
19+
type AnimalType string
20+
21+
// Pets is a collection of Pet objects.
22+
type Pets []*Pet
23+
24+
// NamedPets is a map of animal names to pets.
25+
type NamedPets map[string]*Pet
26+
27+
type (
28+
// Plant represents the plants the user might have and serves as a test
29+
// of structs inside a `type` set.
30+
Plant struct {
31+
Variant string `json:"variant" jsonschema:"title=Variant"` // This comment will be used
32+
// Multicellular is true if the plant is multicellular
33+
Multicellular bool `json:"multicellular,omitempty" jsonschema:"title=Multicellular"` // This comment will be ignored
34+
}
35+
)
36+
type User struct {
37+
// Unique sequential identifier.
38+
ID int `json:"id" jsonschema:"required"`
39+
// This comment will be ignored
40+
Name string `json:"name" jsonschema:"required,minLength=1,maxLength=20,pattern=.*,description=this is a property,title=the name,example=joe,example=lucy,default=alex"`
41+
Friends []int `json:"friends,omitempty" jsonschema_description:"list of IDs, omitted when empty"`
42+
Tags map[string]any `json:"tags,omitempty"`
43+
44+
// An array of pets the user cares for.
45+
Pets Pets `json:"pets"`
46+
47+
// Set of animal names to pets
48+
NamedPets NamedPets `json:"named_pets"`
49+
50+
// Set of plants that the user likes
51+
Plants []*Plant `json:"plants" jsonschema:"title=Plants"`
52+
}
53+
54+
func Test_GenerateJSONSchema_User(t *testing.T) {
55+
s := itool.GenerateJSONSchema(reflect.TypeOf(&User{}))
56+
data, err := json.MarshalIndent(s, "", " ")
57+
if err != nil {
58+
panic(err.Error())
59+
}
60+
t.Log(string(data))
61+
}
62+
63+
func Test_GenerateJSONSchema_TreeNode(t *testing.T) {
64+
s := itool.GenerateJSONSchema(reflect.TypeOf(&TreeNode{}))
65+
data, err := json.MarshalIndent(s, "", " ")
66+
if err != nil {
67+
panic(err.Error())
68+
}
69+
t.Log(string(data))
70+
}
71+
72+
func Test_GenerateJSONSchema_LinkedListNode(t *testing.T) {
73+
s := itool.GenerateJSONSchema(reflect.TypeOf(&LinkedListNode{}))
74+
data, err := json.MarshalIndent(s, "", " ")
75+
if err != nil {
76+
panic(err.Error())
77+
}
78+
t.Log(string(data))
79+
}
80+
81+
type TreeLinkListNode struct {
82+
Name string `json:"name"`
83+
TreeNode *TreeNode `json:"tree_node,omitempty"`
84+
LinkListNode *LinkedListNode `json:"link_list_node,omitempty"`
85+
}
86+
87+
func Test_GenerateJSONSchema_TreeLinkListNode(t *testing.T) {
88+
s := itool.GenerateJSONSchema(reflect.TypeOf(&TreeLinkListNode{}))
89+
data, err := json.MarshalIndent(s, "", " ")
90+
if err != nil {
91+
panic(err.Error())
92+
}
93+
t.Log(string(data))
94+
}

internal/tool/recursive_test.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
//
2+
// Tencent is pleased to support the open source community by making trpc-agent-go available.
3+
//
4+
// Copyright (C) 2025 Tencent. All rights reserved.
5+
//
6+
// trpc-agent-go is licensed under the Apache License Version 2.0.
7+
//
8+
//
9+
10+
package tool_test
11+
12+
import (
13+
"encoding/json"
14+
"reflect"
15+
"testing"
16+
17+
itool "trpc.group/trpc-go/trpc-agent-go/internal/tool"
18+
)
19+
20+
// TreeNode represents a recursive tree structure
21+
type TreeNode struct {
22+
Name string `json:"name"`
23+
Children []*TreeNode `json:"children,omitempty"`
24+
}
25+
26+
// LinkedListNode represents a recursive linked list structure
27+
type LinkedListNode struct {
28+
Value int `json:"value"`
29+
Next *LinkedListNode `json:"next,omitempty"`
30+
}
31+
32+
// MutuallyRecursiveA and MutuallyRecursiveB represent mutually recursive structures
33+
type MutuallyRecursiveA struct {
34+
Name string `json:"name"`
35+
B *MutuallyRecursiveB `json:"b,omitempty"`
36+
}
37+
38+
type MutuallyRecursiveB struct {
39+
Value int `json:"value"`
40+
A *MutuallyRecursiveA `json:"a,omitempty"`
41+
}
42+
43+
func TestGenerateJSONSchema_RecursiveStructure(t *testing.T) {
44+
t.Run("tree node recursive structure", func(t *testing.T) {
45+
// This should not panic and should generate a schema with $ref and $defs
46+
result := itool.GenerateJSONSchema(reflect.TypeOf(TreeNode{}))
47+
resultJson, _ := json.MarshalIndent(result, "", " ")
48+
t.Logf("%s", resultJson)
49+
50+
if result.Type != "object" {
51+
t.Errorf("expected object type, got %s", result.Type)
52+
}
53+
54+
// Check that we have properties
55+
if result.Properties == nil {
56+
t.Fatal("expected properties to be set")
57+
}
58+
59+
// Check name property
60+
if result.Properties["name"] == nil || result.Properties["name"].Type != "string" {
61+
t.Errorf("expected name property of type string")
62+
}
63+
64+
// Check children property
65+
if result.Properties["children"] == nil || result.Properties["children"].Type != "array" {
66+
t.Errorf("expected children property of type array")
67+
}
68+
69+
// The children items should use $ref to avoid infinite recursion
70+
if result.Properties["children"].Items == nil {
71+
t.Fatal("expected children items to be defined")
72+
}
73+
74+
// Check if we have $defs defined for recursive types
75+
if result.Defs == nil {
76+
t.Errorf("expected $defs to be defined for recursive structure")
77+
}
78+
79+
// Check that children items use $ref
80+
if result.Properties["children"].Items.Ref != "#/$defs/treenode" {
81+
t.Errorf("expected children items to reference #/$defs/treenode, got %s", result.Properties["children"].Items.Ref)
82+
}
83+
84+
// Check the definition in $defs
85+
treeDef := result.Defs["treenode"]
86+
if treeDef == nil {
87+
t.Fatal("expected treenode definition in $defs")
88+
}
89+
90+
if treeDef.Type != "object" {
91+
t.Errorf("expected treenode definition type to be object, got %s", treeDef.Type)
92+
}
93+
94+
// Check that the definition also uses $ref for children
95+
if treeDef.Properties["children"].Items == nil || treeDef.Properties["children"].Items.Ref != "#/$defs/treenode" {
96+
t.Errorf("expected treenode definition children items to reference #/$defs/treenode")
97+
}
98+
})
99+
100+
t.Run("linked list recursive structure", func(t *testing.T) {
101+
// This should not panic and should generate a schema with $ref and $defs
102+
result := itool.GenerateJSONSchema(reflect.TypeOf(LinkedListNode{}))
103+
resultJson, _ := json.MarshalIndent(result, "", " ")
104+
t.Logf("%s", resultJson)
105+
106+
if result.Type != "object" {
107+
t.Errorf("expected object type, got %s", result.Type)
108+
}
109+
110+
// Check that we have properties
111+
if result.Properties == nil {
112+
t.Fatal("expected properties to be set")
113+
}
114+
115+
// Check value property
116+
if result.Properties["value"] == nil || result.Properties["value"].Type != "integer" {
117+
t.Errorf("expected value property of type integer")
118+
}
119+
120+
// Check next property - should use $ref to avoid infinite recursion
121+
if result.Properties["next"] == nil {
122+
t.Fatal("expected next property to be defined")
123+
}
124+
125+
// Check if we have $defs defined for recursive types
126+
if result.Defs == nil {
127+
t.Errorf("expected $defs to be defined for recursive structure")
128+
}
129+
})
130+
131+
t.Run("mutually recursive structures", func(t *testing.T) {
132+
// This should not panic and should generate a schema with $ref and $defs
133+
result := itool.GenerateJSONSchema(reflect.TypeOf(MutuallyRecursiveA{}))
134+
resultJson, _ := json.MarshalIndent(result, "", " ")
135+
t.Logf("%s", resultJson)
136+
137+
if result.Type != "object" {
138+
t.Errorf("expected object type, got %s", result.Type)
139+
}
140+
141+
// Check that we have $defs for both types
142+
if result.Defs == nil {
143+
t.Fatal("expected $defs to be defined for mutually recursive structures")
144+
}
145+
146+
// Should have definitions for both types
147+
expectedDefs := 2 // MutuallyRecursiveA and MutuallyRecursiveB
148+
if len(result.Defs) < expectedDefs {
149+
t.Errorf("expected at least %d definitions in $defs, got %d", expectedDefs, len(result.Defs))
150+
}
151+
})
152+
}

0 commit comments

Comments
 (0)