jsonschema: inference ignores invalid types (#147)
Add ForLax[T], which ignores invalid types in schema inference instead of returning an error. This allows additional customization of a schema after inference does what it can. For example, an interface type where all the possible implementations are known can be described with "oneof". For #136.
This commit is contained in:
parent
a519c182e8
commit
c6fabbb3d3
1 changed files with 162 additions and 90 deletions
|
|
@ -13,8 +13,14 @@ import (
|
|||
"github.com/modelcontextprotocol/go-sdk/jsonschema"
|
||||
)
|
||||
|
||||
func forType[T any]() *jsonschema.Schema {
|
||||
s, err := jsonschema.For[T]()
|
||||
func forType[T any](lax bool) *jsonschema.Schema {
|
||||
var s *jsonschema.Schema
|
||||
var err error
|
||||
if lax {
|
||||
s, err = jsonschema.ForLax[T]()
|
||||
} else {
|
||||
s, err = jsonschema.For[T]()
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
@ -28,104 +34,134 @@ func TestFor(t *testing.T) {
|
|||
B int `jsonschema:"bdesc"`
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
type test struct {
|
||||
name string
|
||||
got *jsonschema.Schema
|
||||
want *jsonschema.Schema
|
||||
}{
|
||||
{"string", forType[string](), &schema{Type: "string"}},
|
||||
{"int", forType[int](), &schema{Type: "integer"}},
|
||||
{"int16", forType[int16](), &schema{Type: "integer"}},
|
||||
{"uint32", forType[int16](), &schema{Type: "integer"}},
|
||||
{"float64", forType[float64](), &schema{Type: "number"}},
|
||||
{"bool", forType[bool](), &schema{Type: "boolean"}},
|
||||
{"intmap", forType[map[string]int](), &schema{
|
||||
Type: "object",
|
||||
AdditionalProperties: &schema{Type: "integer"},
|
||||
}},
|
||||
{"anymap", forType[map[string]any](), &schema{
|
||||
Type: "object",
|
||||
AdditionalProperties: &schema{},
|
||||
}},
|
||||
{
|
||||
"struct",
|
||||
forType[struct {
|
||||
F int `json:"f" jsonschema:"fdesc"`
|
||||
G []float64
|
||||
P *bool `jsonschema:"pdesc"`
|
||||
Skip string `json:"-"`
|
||||
NoSkip string `json:",omitempty"`
|
||||
unexported float64
|
||||
unexported2 int `json:"No"`
|
||||
}](),
|
||||
&schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"f": {Type: "integer", Description: "fdesc"},
|
||||
"G": {Type: "array", Items: &schema{Type: "number"}},
|
||||
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
|
||||
"NoSkip": {Type: "string"},
|
||||
},
|
||||
Required: []string{"f", "G", "P"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
},
|
||||
{
|
||||
"no sharing",
|
||||
forType[struct{ X, Y int }](),
|
||||
&schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"X": {Type: "integer"},
|
||||
"Y": {Type: "integer"},
|
||||
},
|
||||
Required: []string{"X", "Y"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested and embedded",
|
||||
forType[struct {
|
||||
A S
|
||||
S
|
||||
}](),
|
||||
&schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"A": {
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"B": {Type: "integer", Description: "bdesc"},
|
||||
},
|
||||
Required: []string{"B"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
"S": {
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"B": {Type: "integer", Description: "bdesc"},
|
||||
},
|
||||
Required: []string{"B"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
},
|
||||
Required: []string{"A", "S"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if diff := cmp.Diff(test.want, test.got, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
|
||||
t.Fatalf("ForType mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
// These schemas should all resolve.
|
||||
if _, err := test.got.Resolve(nil); err != nil {
|
||||
t.Fatalf("Resolving: %v", err)
|
||||
}
|
||||
})
|
||||
tests := func(lax bool) []test {
|
||||
return []test{
|
||||
{"string", forType[string](lax), &schema{Type: "string"}},
|
||||
{"int", forType[int](lax), &schema{Type: "integer"}},
|
||||
{"int16", forType[int16](lax), &schema{Type: "integer"}},
|
||||
{"uint32", forType[int16](lax), &schema{Type: "integer"}},
|
||||
{"float64", forType[float64](lax), &schema{Type: "number"}},
|
||||
{"bool", forType[bool](lax), &schema{Type: "boolean"}},
|
||||
{"intmap", forType[map[string]int](lax), &schema{
|
||||
Type: "object",
|
||||
AdditionalProperties: &schema{Type: "integer"},
|
||||
}},
|
||||
{"anymap", forType[map[string]any](lax), &schema{
|
||||
Type: "object",
|
||||
AdditionalProperties: &schema{},
|
||||
}},
|
||||
{
|
||||
"struct",
|
||||
forType[struct {
|
||||
F int `json:"f" jsonschema:"fdesc"`
|
||||
G []float64
|
||||
P *bool `jsonschema:"pdesc"`
|
||||
Skip string `json:"-"`
|
||||
NoSkip string `json:",omitempty"`
|
||||
unexported float64
|
||||
unexported2 int `json:"No"`
|
||||
}](lax),
|
||||
&schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"f": {Type: "integer", Description: "fdesc"},
|
||||
"G": {Type: "array", Items: &schema{Type: "number"}},
|
||||
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
|
||||
"NoSkip": {Type: "string"},
|
||||
},
|
||||
Required: []string{"f", "G", "P"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
},
|
||||
{
|
||||
"no sharing",
|
||||
forType[struct{ X, Y int }](lax),
|
||||
&schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"X": {Type: "integer"},
|
||||
"Y": {Type: "integer"},
|
||||
},
|
||||
Required: []string{"X", "Y"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
},
|
||||
{
|
||||
"nested and embedded",
|
||||
forType[struct {
|
||||
A S
|
||||
S
|
||||
}](lax),
|
||||
&schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"A": {
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"B": {Type: "integer", Description: "bdesc"},
|
||||
},
|
||||
Required: []string{"B"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
"S": {
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"B": {Type: "integer", Description: "bdesc"},
|
||||
},
|
||||
Required: []string{"B"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
},
|
||||
Required: []string{"A", "S"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
run := func(t *testing.T, tt test) {
|
||||
if diff := cmp.Diff(tt.want, tt.got, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
|
||||
t.Fatalf("ForType mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
// These schemas should all resolve.
|
||||
if _, err := tt.got.Resolve(nil); err != nil {
|
||||
t.Fatalf("Resolving: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("strict", func(t *testing.T) {
|
||||
for _, test := range tests(false) {
|
||||
t.Run(test.name, func(t *testing.T) { run(t, test) })
|
||||
}
|
||||
})
|
||||
|
||||
laxTests := append(tests(true), test{
|
||||
"ignore",
|
||||
forType[struct {
|
||||
A int
|
||||
B map[int]int
|
||||
C func()
|
||||
}](true),
|
||||
&schema{
|
||||
Type: "object",
|
||||
Properties: map[string]*schema{
|
||||
"A": {Type: "integer"},
|
||||
},
|
||||
Required: []string{"A"},
|
||||
AdditionalProperties: falseSchema(),
|
||||
},
|
||||
})
|
||||
t.Run("lax", func(t *testing.T) {
|
||||
for _, test := range laxTests {
|
||||
t.Run(test.name, func(t *testing.T) { run(t, test) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func forErr[T any]() error {
|
||||
|
|
|
|||
Loading…
Reference in a new issue