diff --git a/jsonschema/infer.go b/jsonschema/infer.go index 7b6b7e2b..080d9d09 100644 --- a/jsonschema/infer.go +++ b/jsonschema/infer.go @@ -9,6 +9,7 @@ package jsonschema import ( "fmt" "log/slog" + "maps" "math/big" "reflect" "regexp" @@ -19,8 +20,8 @@ import ( // ForOptions are options for the [For] function. type ForOptions struct { - // If IgnoreInvalidTypes is true, fields that can't be represented as a JSON Schema - // are ignored instead of causing an error. + // If IgnoreInvalidTypes is true, fields that can't be represented as a JSON + // Schema are ignored instead of causing an error. // This allows callers to adjust the resulting schema using custom knowledge. // For example, an interface type where all the possible implementations are // known can be described with "oneof". @@ -77,15 +78,7 @@ func For[T any](opts *ForOptions) (*Schema, error) { if opts == nil { opts = &ForOptions{} } - schemas := make(map[reflect.Type]*Schema) - // Add types from the standard library that have MarshalJSON methods. - ss := &Schema{Type: "string"} - schemas[reflect.TypeFor[time.Time]()] = ss - schemas[reflect.TypeFor[slog.Level]()] = ss - schemas[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}} - schemas[reflect.TypeFor[big.Rat]()] = ss - schemas[reflect.TypeFor[big.Float]()] = ss - + schemas := maps.Clone(initialSchemaMap) // Add types from the options. They override the default ones. for v, s := range opts.TypeSchemas { schemas[reflect.TypeOf(v)] = s @@ -98,6 +91,20 @@ func For[T any](opts *ForOptions) (*Schema, error) { return s, nil } +// ForType is like [For], but takes a [reflect.Type] +func ForType(t reflect.Type, opts *ForOptions) (*Schema, error) { + schemas := maps.Clone(initialSchemaMap) + // Add types from the options. They override the default ones. + for v, s := range opts.TypeSchemas { + schemas[reflect.TypeOf(v)] = s + } + s, err := forType(t, map[reflect.Type]bool{}, opts.IgnoreInvalidTypes, schemas) + if err != nil { + return nil, fmt.Errorf("ForType(%s): %w", t, err) + } + return s, nil +} + func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas map[reflect.Type]*Schema) (*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. @@ -230,5 +237,17 @@ func forType(t reflect.Type, seen map[reflect.Type]bool, ignore bool, schemas ma return s, nil } +// initialSchemaMap holds types from the standard library that have MarshalJSON methods. +var initialSchemaMap = make(map[reflect.Type]*Schema) + +func init() { + ss := &Schema{Type: "string"} + initialSchemaMap[reflect.TypeFor[time.Time]()] = ss + initialSchemaMap[reflect.TypeFor[slog.Level]()] = ss + initialSchemaMap[reflect.TypeFor[big.Int]()] = &Schema{Types: []string{"null", "string"}} + initialSchemaMap[reflect.TypeFor[big.Rat]()] = ss + initialSchemaMap[reflect.TypeFor[big.Float]()] = ss +} + // Disallow jsonschema tag values beginning "WORD=", for future expansion. var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=") diff --git a/jsonschema/infer_test.go b/jsonschema/infer_test.go index 1a0895b4..62bfbbbc 100644 --- a/jsonschema/infer_test.go +++ b/jsonschema/infer_test.go @@ -7,6 +7,7 @@ package jsonschema_test import ( "log/slog" "math/big" + "reflect" "strings" "testing" "time" @@ -139,7 +140,7 @@ func TestFor(t *testing.T) { 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) + t.Fatalf("For mismatch (-want +got):\n%s", diff) } // These schemas should all resolve. if _, err := tt.got.Resolve(nil); err != nil { @@ -176,6 +177,40 @@ func TestFor(t *testing.T) { }) } +func TestForType(t *testing.T) { + type schema = jsonschema.Schema + + // ForType is virtually identical to For. Just test that options are handled properly. + opts := &jsonschema.ForOptions{ + IgnoreInvalidTypes: true, + TypeSchemas: map[any]*jsonschema.Schema{ + custom(0): {Type: "custom"}, + }, + } + + type S struct { + I int + F func() + C custom + } + got, err := jsonschema.ForType(reflect.TypeOf(S{}), opts) + if err != nil { + t.Fatal(err) + } + want := &schema{ + Type: "object", + Properties: map[string]*schema{ + "I": {Type: "integer"}, + "C": {Type: "custom"}, + }, + Required: []string{"I", "C"}, + AdditionalProperties: falseSchema(), + } + if diff := cmp.Diff(want, got, cmpopts.IgnoreUnexported(schema{})); diff != "" { + t.Fatalf("ForType mismatch (-want +got):\n%s", diff) + } +} + func forErr[T any]() error { _, err := jsonschema.For[T](nil) return err