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 (
|
||||
"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]*=")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue