jsonschema: add ForType function (#242)
Add ForType, which is like For but takes a reflect.Type. Fixes #233.
This commit is contained in:
parent
be0bd77f48
commit
032f03bbea
2 changed files with 66 additions and 12 deletions
|
|
@ -9,6 +9,7 @@ package jsonschema
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"maps"
|
||||||
"math/big"
|
"math/big"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -19,8 +20,8 @@ import (
|
||||||
|
|
||||||
// ForOptions are options for the [For] function.
|
// ForOptions are options for the [For] function.
|
||||||
type ForOptions struct {
|
type ForOptions struct {
|
||||||
// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON Schema
|
// If IgnoreInvalidTypes is true, fields that can't be represented as a JSON
|
||||||
// are ignored instead of causing an error.
|
// Schema are ignored instead of causing an error.
|
||||||
// This allows callers to adjust the resulting schema using custom knowledge.
|
// This allows callers to adjust the resulting schema using custom knowledge.
|
||||||
// For example, an interface type where all the possible implementations are
|
// For example, an interface type where all the possible implementations are
|
||||||
// known can be described with "oneof".
|
// known can be described with "oneof".
|
||||||
|
|
@ -77,15 +78,7 @@ func For[T any](opts *ForOptions) (*Schema, error) {
|
||||||
if opts == nil {
|
if opts == nil {
|
||||||
opts = &ForOptions{}
|
opts = &ForOptions{}
|
||||||
}
|
}
|
||||||
schemas := make(map[reflect.Type]*Schema)
|
schemas := maps.Clone(initialSchemaMap)
|
||||||
// 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
|
|
||||||
|
|
||||||
// Add types from the options. They override the default ones.
|
// Add types from the options. They override the default ones.
|
||||||
for v, s := range opts.TypeSchemas {
|
for v, s := range opts.TypeSchemas {
|
||||||
schemas[reflect.TypeOf(v)] = s
|
schemas[reflect.TypeOf(v)] = s
|
||||||
|
|
@ -98,6 +91,20 @@ func For[T any](opts *ForOptions) (*Schema, error) {
|
||||||
return s, nil
|
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) {
|
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
|
// Follow pointers: the schema for *T is almost the same as for T, except that
|
||||||
// an explicit JSON "null" is allowed for the pointer.
|
// 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
|
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.
|
// Disallow jsonschema tag values beginning "WORD=", for future expansion.
|
||||||
var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")
|
var disallowedPrefixRegexp = regexp.MustCompile("^[^ \t\n]*=")
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ package jsonschema_test
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -139,7 +140,7 @@ func TestFor(t *testing.T) {
|
||||||
|
|
||||||
run := func(t *testing.T, tt test) {
|
run := func(t *testing.T, tt test) {
|
||||||
if diff := cmp.Diff(tt.want, tt.got, cmpopts.IgnoreUnexported(jsonschema.Schema{})); diff != "" {
|
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.
|
// These schemas should all resolve.
|
||||||
if _, err := tt.got.Resolve(nil); err != nil {
|
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 {
|
func forErr[T any]() error {
|
||||||
_, err := jsonschema.For[T](nil)
|
_, err := jsonschema.For[T](nil)
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue