Skip to content

Commit 79a27e1

Browse files
committed
add ForLax instead
1 parent f364a20 commit 79a27e1

File tree

2 files changed

+162
-119
lines changed

2 files changed

+162
-119
lines changed

jsonschema/infer.go

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,8 @@ import (
3030
// struct field JSON name. Fields that are marked "omitempty" are
3131
// considered optional; all other fields become required properties.
3232
//
33-
// For ignores the following Go types, as they are incompatible with the JSON schema spec.
34-
// Since it ignores them instead of failing, you can call this function first, then adjust
35-
// the result yourself to handle these types.
36-
//
33+
// For returns an error if t contains (possibly recursively) any of the following Go
34+
// types, as they are incompatible with the JSON schema spec.
3735
// - maps with key other than 'string'
3836
// - function types
3937
// - channel types
@@ -49,15 +47,30 @@ import (
4947
func For[T any]() (*Schema, error) {
5048
// TODO: consider skipping incompatible fields, instead of failing.
5149
seen := make(map[reflect.Type]bool)
52-
s, err := forType(reflect.TypeFor[T](), seen)
50+
s, err := forType(reflect.TypeFor[T](), seen, false)
5351
if err != nil {
5452
var z T
5553
return nil, fmt.Errorf("For[%T](): %w", z, err)
5654
}
5755
return s, nil
5856
}
5957

60-
func forType(t reflect.Type, seen map[reflect.Type]bool) (*Schema, error) {
58+
// ForLax behaves like [For], except that it ignores struct fields with invalid types instead of
59+
// returning an error. That allows callers to adjust the resulting schema using custom knowledge.
60+
// For example, an interface type where all the possible implementations are known
61+
// can be described with "oneof".
62+
func ForLax[T any]() (*Schema, error) {
63+
// TODO: consider skipping incompatible fields, instead of failing.
64+
seen := make(map[reflect.Type]bool)
65+
s, err := forType(reflect.TypeFor[T](), seen, true)
66+
if err != nil {
67+
var z T
68+
return nil, fmt.Errorf("ForLax[%T](): %w", z, err)
69+
}
70+
return s, nil
71+
}
72+
73+
func forType(t reflect.Type, seen map[reflect.Type]bool, lax bool) (*Schema, error) {
6174
// Follow pointers: the schema for *T is almost the same as for T, except that
6275
// an explicit JSON "null" is allowed for the pointer.
6376
allowNull := false
@@ -98,26 +111,31 @@ func forType(t reflect.Type, seen map[reflect.Type]bool) (*Schema, error) {
98111

99112
case reflect.Map:
100113
if t.Key().Kind() != reflect.String {
101-
return nil, nil
114+
if lax {
115+
return nil, nil // ignore
116+
}
117+
return nil, fmt.Errorf("unsupported map key type %v", t.Key().Kind())
118+
}
119+
if t.Key().Kind() != reflect.String {
102120
}
103121
s.Type = "object"
104-
s.AdditionalProperties, err = forType(t.Elem(), seen)
122+
s.AdditionalProperties, err = forType(t.Elem(), seen, lax)
105123
if err != nil {
106124
return nil, fmt.Errorf("computing map value schema: %v", err)
107125
}
108-
// Ignore if the element type is invalid.
109-
if s.AdditionalProperties == nil {
126+
if lax && s.AdditionalProperties == nil {
127+
// Ignore if the element type is invalid.
110128
return nil, nil
111129
}
112130

113131
case reflect.Slice, reflect.Array:
114132
s.Type = "array"
115-
s.Items, err = forType(t.Elem(), seen)
133+
s.Items, err = forType(t.Elem(), seen, lax)
116134
if err != nil {
117135
return nil, fmt.Errorf("computing element schema: %v", err)
118136
}
119-
// Ignore if the element type is invalid.
120-
if s.Items == nil {
137+
if lax && s.Items == nil {
138+
// Ignore if the element type is invalid.
121139
return nil, nil
122140
}
123141
if t.Kind() == reflect.Array {
@@ -142,12 +160,12 @@ func forType(t reflect.Type, seen map[reflect.Type]bool) (*Schema, error) {
142160
if s.Properties == nil {
143161
s.Properties = make(map[string]*Schema)
144162
}
145-
fs, err := forType(field.Type, seen)
163+
fs, err := forType(field.Type, seen, lax)
146164
if err != nil {
147165
return nil, err
148166
}
149-
// Skip fields of invalid type.
150-
if fs == nil {
167+
if lax && fs == nil {
168+
// Skip fields of invalid type.
151169
continue
152170
}
153171
if tag, ok := field.Tag.Lookup("jsonschema"); ok {
@@ -166,8 +184,11 @@ func forType(t reflect.Type, seen map[reflect.Type]bool) (*Schema, error) {
166184
}
167185

168186
default:
169-
// Ignore.
170-
return nil, nil
187+
if lax {
188+
// Ignore.
189+
return nil, nil
190+
}
191+
return nil, fmt.Errorf("type %v is unsupported by jsonschema", t)
171192
}
172193
if allowNull && s.Type != "" {
173194
s.Types = []string{"null", s.Type}

jsonschema/infer_test.go

Lines changed: 123 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,14 @@ import (
1313
"github.com/modelcontextprotocol/go-sdk/jsonschema"
1414
)
1515

16-
func forType[T any]() *jsonschema.Schema {
17-
s, err := jsonschema.For[T]()
16+
func forType[T any](lax bool) *jsonschema.Schema {
17+
var s *jsonschema.Schema
18+
var err error
19+
if lax {
20+
s, err = jsonschema.ForLax[T]()
21+
} else {
22+
s, err = jsonschema.For[T]()
23+
}
1824
if err != nil {
1925
panic(err)
2026
}
@@ -28,120 +34,134 @@ func TestFor(t *testing.T) {
2834
B int `jsonschema:"bdesc"`
2935
}
3036

31-
tests := []struct {
37+
type test struct {
3238
name string
3339
got *jsonschema.Schema
3440
want *jsonschema.Schema
35-
}{
36-
{"string", forType[string](), &schema{Type: "string"}},
37-
{"int", forType[int](), &schema{Type: "integer"}},
38-
{"int16", forType[int16](), &schema{Type: "integer"}},
39-
{"uint32", forType[int16](), &schema{Type: "integer"}},
40-
{"float64", forType[float64](), &schema{Type: "number"}},
41-
{"bool", forType[bool](), &schema{Type: "boolean"}},
42-
{"intmap", forType[map[string]int](), &schema{
43-
Type: "object",
44-
AdditionalProperties: &schema{Type: "integer"},
45-
}},
46-
{"anymap", forType[map[string]any](), &schema{
47-
Type: "object",
48-
AdditionalProperties: &schema{},
49-
}},
50-
{
51-
"struct",
52-
forType[struct {
53-
F int `json:"f" jsonschema:"fdesc"`
54-
G []float64
55-
P *bool `jsonschema:"pdesc"`
56-
Skip string `json:"-"`
57-
NoSkip string `json:",omitempty"`
58-
unexported float64
59-
unexported2 int `json:"No"`
60-
}](),
61-
&schema{
62-
Type: "object",
63-
Properties: map[string]*schema{
64-
"f": {Type: "integer", Description: "fdesc"},
65-
"G": {Type: "array", Items: &schema{Type: "number"}},
66-
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
67-
"NoSkip": {Type: "string"},
41+
}
42+
43+
tests := func(lax bool) []test {
44+
return []test{
45+
{"string", forType[string](lax), &schema{Type: "string"}},
46+
{"int", forType[int](lax), &schema{Type: "integer"}},
47+
{"int16", forType[int16](lax), &schema{Type: "integer"}},
48+
{"uint32", forType[int16](lax), &schema{Type: "integer"}},
49+
{"float64", forType[float64](lax), &schema{Type: "number"}},
50+
{"bool", forType[bool](lax), &schema{Type: "boolean"}},
51+
{"intmap", forType[map[string]int](lax), &schema{
52+
Type: "object",
53+
AdditionalProperties: &schema{Type: "integer"},
54+
}},
55+
{"anymap", forType[map[string]any](lax), &schema{
56+
Type: "object",
57+
AdditionalProperties: &schema{},
58+
}},
59+
{
60+
"struct",
61+
forType[struct {
62+
F int `json:"f" jsonschema:"fdesc"`
63+
G []float64
64+
P *bool `jsonschema:"pdesc"`
65+
Skip string `json:"-"`
66+
NoSkip string `json:",omitempty"`
67+
unexported float64
68+
unexported2 int `json:"No"`
69+
}](lax),
70+
&schema{
71+
Type: "object",
72+
Properties: map[string]*schema{
73+
"f": {Type: "integer", Description: "fdesc"},
74+
"G": {Type: "array", Items: &schema{Type: "number"}},
75+
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
76+
"NoSkip": {Type: "string"},
77+
},
78+
Required: []string{"f", "G", "P"},
79+
AdditionalProperties: falseSchema(),
6880
},
69-
Required: []string{"f", "G", "P"},
70-
AdditionalProperties: falseSchema(),
7181
},
72-
},
73-
{
74-
"no sharing",
75-
forType[struct{ X, Y int }](),
76-
&schema{
77-
Type: "object",
78-
Properties: map[string]*schema{
79-
"X": {Type: "integer"},
80-
"Y": {Type: "integer"},
82+
{
83+
"no sharing",
84+
forType[struct{ X, Y int }](lax),
85+
&schema{
86+
Type: "object",
87+
Properties: map[string]*schema{
88+
"X": {Type: "integer"},
89+
"Y": {Type: "integer"},
90+
},
91+
Required: []string{"X", "Y"},
92+
AdditionalProperties: falseSchema(),
8193
},
82-
Required: []string{"X", "Y"},
83-
AdditionalProperties: falseSchema(),
8494
},
85-
},
86-
{
87-
"nested and embedded",
88-
forType[struct {
89-
A S
90-
S
91-
}](),
92-
&schema{
93-
Type: "object",
94-
Properties: map[string]*schema{
95-
"A": {
96-
Type: "object",
97-
Properties: map[string]*schema{
98-
"B": {Type: "integer", Description: "bdesc"},
95+
{
96+
"nested and embedded",
97+
forType[struct {
98+
A S
99+
S
100+
}](lax),
101+
&schema{
102+
Type: "object",
103+
Properties: map[string]*schema{
104+
"A": {
105+
Type: "object",
106+
Properties: map[string]*schema{
107+
"B": {Type: "integer", Description: "bdesc"},
108+
},
109+
Required: []string{"B"},
110+
AdditionalProperties: falseSchema(),
99111
},
100-
Required: []string{"B"},
101-
AdditionalProperties: falseSchema(),
102-
},
103-
"S": {
104-
Type: "object",
105-
Properties: map[string]*schema{
106-
"B": {Type: "integer", Description: "bdesc"},
112+
"S": {
113+
Type: "object",
114+
Properties: map[string]*schema{
115+
"B": {Type: "integer", Description: "bdesc"},
116+
},
117+
Required: []string{"B"},
118+
AdditionalProperties: falseSchema(),
107119
},
108-
Required: []string{"B"},
109-
AdditionalProperties: falseSchema(),
110120
},
121+
Required: []string{"A", "S"},
122+
AdditionalProperties: falseSchema(),
111123
},
112-
Required: []string{"A", "S"},
113-
AdditionalProperties: falseSchema(),
114124
},
115-
},
116-
{
117-
"ignore",
118-
forType[struct {
119-
A int
120-
B map[int]int
121-
C func()
122-
}](),
123-
&schema{
124-
Type: "object",
125-
Properties: map[string]*schema{
126-
"A": {Type: "integer"},
127-
},
128-
Required: []string{"A"},
129-
AdditionalProperties: falseSchema(),
130-
},
131-
},
125+
}
132126
}
133127

134-
for _, test := range tests {
135-
t.Run(test.name, func(t *testing.T) {
136-
if diff := cmp.Diff(test.want, test.got, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
137-
t.Fatalf("ForType mismatch (-want +got):\n%s", diff)
138-
}
139-
// These schemas should all resolve.
140-
if _, err := test.got.Resolve(nil); err != nil {
141-
t.Fatalf("Resolving: %v", err)
142-
}
143-
})
128+
run := func(t *testing.T, tt test) {
129+
if diff := cmp.Diff(tt.want, tt.got, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
130+
t.Fatalf("ForType mismatch (-want +got):\n%s", diff)
131+
}
132+
// These schemas should all resolve.
133+
if _, err := tt.got.Resolve(nil); err != nil {
134+
t.Fatalf("Resolving: %v", err)
135+
}
144136
}
137+
138+
t.Run("strict", func(t *testing.T) {
139+
for _, test := range tests(false) {
140+
t.Run(test.name, func(t *testing.T) { run(t, test) })
141+
}
142+
})
143+
144+
laxTests := append(tests(true), test{
145+
"ignore",
146+
forType[struct {
147+
A int
148+
B map[int]int
149+
C func()
150+
}](true),
151+
&schema{
152+
Type: "object",
153+
Properties: map[string]*schema{
154+
"A": {Type: "integer"},
155+
},
156+
Required: []string{"A"},
157+
AdditionalProperties: falseSchema(),
158+
},
159+
})
160+
t.Run("lax", func(t *testing.T) {
161+
for _, test := range laxTests {
162+
t.Run(test.name, func(t *testing.T) { run(t, test) })
163+
}
164+
})
145165
}
146166

147167
func forErr[T any]() error {
@@ -163,8 +183,10 @@ func TestForErrors(t *testing.T) {
163183
got error
164184
want string
165185
}{
186+
{forErr[map[int]int](), "unsupported map key type"},
166187
{forErr[s1](), "empty jsonschema tag"},
167188
{forErr[s2](), "must not begin with"},
189+
{forErr[func()](), "unsupported"},
168190
} {
169191
if tt.got == nil {
170192
t.Errorf("got nil, want error containing %q", tt.want)

0 commit comments

Comments
 (0)