jsonchema: add cycle detection (#97)
This PR add cycle detection where it return error instead of waiting for stack overflow. This is a fix for https://github.com/modelcontextprotocol/go-sdk/issues/77. The root cause of the issue was the type being processed is deeply nested and has cycle. Hence, the heap memory growth is faster than the stack growth, it went OOM before stack overflow panic can kick in.
This commit is contained in:
parent
2b07560be0
commit
e27a36ed32
1 changed files with 110 additions and 5 deletions
|
|
@ -37,9 +37,11 @@ import (
|
|||
// - unsafe pointers
|
||||
//
|
||||
// The types must not have cycles.
|
||||
// It will return an error if there is a cycle in the types.
|
||||
func For[T any]() (*Schema, error) {
|
||||
// TODO: consider skipping incompatible fields, instead of failing.
|
||||
s, err := forType(reflect.TypeFor[T]())
|
||||
seen := make(map[reflect.Type]bool)
|
||||
s, err := forType(reflect.TypeFor[T](), seen)
|
||||
if err != nil {
|
||||
var z T
|
||||
return nil, fmt.Errorf("For[%T](): %w", z, err)
|
||||
|
|
@ -47,7 +49,7 @@ func For[T any]() (*Schema, error) {
|
|||
return s, nil
|
||||
}
|
||||
|
||||
func forType(t reflect.Type) (*Schema, error) {
|
||||
func forType(t reflect.Type, seen map[reflect.Type]bool) (*Schema, error) {
|
||||
// Follow pointers: the schema for *T is almost the same as for T, except that
|
||||
// an explicit JSON "null" is allowed for the pointer.
|
||||
allowNull := false
|
||||
|
|
@ -56,6 +58,16 @@ func forType(t reflect.Type) (*Schema, error) {
|
|||
t = t.Elem()
|
||||
}
|
||||
|
||||
// Check for cycles
|
||||
// User defined types have a name, so we can skip those that are natively defined
|
||||
if t.Name() != "" {
|
||||
if seen[t] {
|
||||
return nil, fmt.Errorf("cycle detected for type %v", t)
|
||||
}
|
||||
seen[t] = true
|
||||
defer delete(seen, t)
|
||||
}
|
||||
|
||||
var (
|
||||
s = new(Schema)
|
||||
err error
|
||||
|
|
@ -81,14 +93,14 @@ func forType(t reflect.Type) (*Schema, error) {
|
|||
return nil, fmt.Errorf("unsupported map key type %v", t.Key().Kind())
|
||||
}
|
||||
s.Type = "object"
|
||||
s.AdditionalProperties, err = forType(t.Elem())
|
||||
s.AdditionalProperties, err = forType(t.Elem(), seen)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing map value schema: %v", err)
|
||||
}
|
||||
|
||||
case reflect.Slice, reflect.Array:
|
||||
s.Type = "array"
|
||||
s.Items, err = forType(t.Elem())
|
||||
s.Items, err = forType(t.Elem(), seen)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("computing element schema: %v", err)
|
||||
}
|
||||
|
|
@ -114,7 +126,7 @@ 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)
|
||||
s.Properties[info.Name], err = forType(field.Type, seen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue