Skip to content

jsonschema: support "jsonschema" struct tags #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion jsonschema/infer.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package jsonschema
import (
"fmt"
"reflect"
"regexp"

"github.com/modelcontextprotocol/go-sdk/internal/util"
)
Expand Down Expand Up @@ -37,6 +38,11 @@ import (
// - unsafe pointers
//
// The types must not have cycles.
//
// For recognizes struct field tags named "jsonschema".
// A jsonschema tag on a field is used as the description for the corresponding property.
// For future compatibility, descriptions must not start with "WORD=", where WORD is a
// sequence of non-whitespace characters.
func For[T any]() (*Schema, error) {
// TODO: consider skipping incompatible fields, instead of failing.
s, err := forType(reflect.TypeFor[T]())
Expand Down Expand Up @@ -114,7 +120,20 @@ func forType(t reflect.Type) (*Schema, error) {
if s.Properties == nil {
s.Properties = make(map[string]*Schema)
}
s.Properties[info.Name], err = forType(field.Type)
fs, err := forType(field.Type)
if err != nil {
return nil, err
}
if tag, ok := field.Tag.Lookup("jsonschema"); ok {
if tag == "" {
return nil, fmt.Errorf("empty jsonschema tag on struct field %s.%s", t, field.Name)
}
if disallowedPrefixRegexp.MatchString(tag) {
return nil, fmt.Errorf("tag must not begin with 'WORD=': %q", tag)
}
fs.Description = tag
}
s.Properties[info.Name] = fs
if err != nil {
return nil, err
}
Expand All @@ -132,3 +151,6 @@ func forType(t reflect.Type) (*Schema, error) {
}
return s, nil
}

// Disallow jsonschema tag values beginning "WORD=", for future expansion.
var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")
51 changes: 45 additions & 6 deletions jsonschema/infer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ func forType[T any]() *jsonschema.Schema {

func TestForType(t *testing.T) {
type schema = jsonschema.Schema

type S struct {
B int `jsonschema:"bdesc"`
}

tests := []struct {
name string
got *jsonschema.Schema
Expand All @@ -44,9 +49,9 @@ func TestForType(t *testing.T) {
{
"struct",
forType[struct {
F int `json:"f"`
F int `json:"f" jsonschema:"fdesc"`
G []float64
P *bool
P *bool `jsonschema:"pdesc"`
Skip string `json:"-"`
NoSkip string `json:",omitempty"`
unexported float64
Expand All @@ -55,13 +60,13 @@ func TestForType(t *testing.T) {
&schema{
Type: "object",
Properties: map[string]*schema{
"f": {Type: "integer"},
"f": {Type: "integer", Description: "fdesc"},
"G": {Type: "array", Items: &schema{Type: "number"}},
"P": {Types: []string{"null", "boolean"}},
"P": {Types: []string{"null", "boolean"}, Description: "pdesc"},
"NoSkip": {Type: "string"},
},
Required: []string{"f", "G", "P"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
AdditionalProperties: falseSchema(),
},
},
{
Expand All @@ -74,7 +79,37 @@ func TestForType(t *testing.T) {
"Y": {Type: "integer"},
},
Required: []string{"X", "Y"},
AdditionalProperties: &jsonschema.Schema{Not: &jsonschema.Schema{}},
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(),
},
},
}
Expand All @@ -91,3 +126,7 @@ func TestForType(t *testing.T) {
})
}
}

func falseSchema() *jsonschema.Schema {
return &jsonschema.Schema{Not: &jsonschema.Schema{}}
}
Loading