diff --git a/fixtures/reflect_with_handle.json b/fixtures/reflect_with_handle.json new file mode 100644 index 0000000..98bc3cc --- /dev/null +++ b/fixtures/reflect_with_handle.json @@ -0,0 +1,77 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/invopop/jsonschema/handle-test", + "$ref": "#/$defs/HandleTest", + "$defs": { + "HandleTest": { + "oneOf": [ + { + "$ref": "#/$defs/TypeOne" + }, + { + "$ref": "#/$defs/TypeTwo" + }, + { + "$ref": "#/$defs/RecursiveType" + } + ] + }, + "RecursiveType": { + "properties": { + "type": { + "type": "string", + "enum": [ + "recursive" + ] + }, + "self": { + "$ref": "#/$defs/RecursiveType" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "self" + ] + }, + "TypeOne": { + "properties": { + "type": { + "type": "string", + "enum": [ + "one" + ] + }, + "oneField": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "oneField" + ] + }, + "TypeTwo": { + "properties": { + "type": { + "type": "string", + "enum": [ + "two" + ] + }, + "twoField": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "type", + "twoField" + ] + } + } +} \ No newline at end of file diff --git a/reflect.go b/reflect.go index 3249c8c..5ce8ca2 100644 --- a/reflect.go +++ b/reflect.go @@ -24,6 +24,15 @@ type customSchemaImpl interface { JSONSchema() *Schema } +// customSchemaHandleImpl is used to detect if the type provides it's own +// custom Schema Type definition to use instead. +// +// This is for use in scenarios where customSchemaImpl references other custom +// type also need to be custom. +type customSchemaHandleImpl interface { + JSONSchemaHandle(ReflectorHandle) *Schema +} + // Function to be run after the schema has been generated. // this will let you modify a schema afterwards type extendSchemaImpl interface { @@ -43,11 +52,16 @@ type propertyAliasSchemaImpl interface { JSONSchemaProperty(prop string) any } -var customAliasSchema = reflect.TypeOf((*aliasSchemaImpl)(nil)).Elem() -var customPropertyAliasSchema = reflect.TypeOf((*propertyAliasSchemaImpl)(nil)).Elem() +var ( + customAliasSchema = reflect.TypeOf((*aliasSchemaImpl)(nil)).Elem() + customPropertyAliasSchema = reflect.TypeOf((*propertyAliasSchemaImpl)(nil)).Elem() +) -var customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem() -var extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem() +var ( + customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem() + customHandleType = reflect.TypeOf((*customSchemaHandleImpl)(nil)).Elem() + extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem() +) // customSchemaGetFieldDocString type customSchemaGetFieldDocString interface { @@ -131,6 +145,9 @@ type Reflector struct { // Mapper is a function that can be used to map custom Go types to jsonschema schemas. Mapper func(reflect.Type) *Schema + // Mapper is a function that can be used to map custom Go types to jsonschema schemas. + MapperHandle func(ReflectorHandle, reflect.Type) *Schema + // Namer allows customizing of type names. The default is to use the type's name // provided by the reflect package. Namer func(reflect.Type) string @@ -284,9 +301,17 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type) return t } } + if r.MapperHandle != nil { + if t := r.MapperHandle(r.apiHandle(definitions), t); t != nil { + return t + } + } if rt := r.reflectCustomSchema(definitions, t); rt != nil { return rt } + if rt := r.reflectSchemaWithHandle(definitions, t); rt != nil { + return rt + } // Prepare a base to which details can be added st := new(Schema) @@ -370,6 +395,49 @@ func (r *Reflector) reflectCustomSchema(definitions Definitions, t reflect.Type) return nil } +// api for reflection with an api handle +type ReflectorHandle struct { + SchemaFor func(t any) *Schema + SchemaForType func(t reflect.Type) *Schema +} + +func (r *Reflector) apiHandle(definitions Definitions) ReflectorHandle { + return ReflectorHandle{ + SchemaFor: func(v any) *Schema { + t := reflect.TypeOf(v) + if v == nil { + return nil + } + return r.refOrReflectTypeToSchema(definitions, t) + }, + SchemaForType: func(t reflect.Type) *Schema { + if t == nil { + return nil + } + return r.refOrReflectTypeToSchema(definitions, t) + }, + } +} + +func (r *Reflector) reflectSchemaWithHandle(definitions Definitions, t reflect.Type) *Schema { + if t.Kind() == reflect.Ptr { + return r.reflectSchemaWithHandle(definitions, t.Elem()) + } + + if t.Implements(customHandleType) { + v := reflect.New(t) + o := v.Interface().(customSchemaHandleImpl) + st := o.JSONSchemaHandle(r.apiHandle(definitions)) + r.addDefinition(definitions, t, st) + if ref := r.refDefinition(definitions, t); ref != nil { + return ref + } + return st + } + + return nil +} + func (r *Reflector) reflectSchemaExtend(definitions Definitions, t reflect.Type, s *Schema) *Schema { if t.Implements(extendType) { v := reflect.New(t) diff --git a/reflect_test.go b/reflect_test.go index 94b6018..5dc2a5b 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -14,7 +14,6 @@ import ( "time" "github.com/invopop/jsonschema/examples" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -337,6 +336,32 @@ type PatternEqualsTest struct { WithEqualsAndCommas string `jsonschema:"pattern=foo\\,=bar"` } +type HandleTest struct { +} +type TypeOne struct { + Type string `json:"type" jsonschema:"required,enum=one"` + OneField string `json:"oneField"` +} +type TypeTwo struct { + Type string `json:"type" jsonschema:"required,enum=two"` + TwoField string `json:"twoField"` +} +type RecursiveType struct { + Type string `json:"type" jsonschema:"required,enum=recursive"` + Self *RecursiveType `json:"self"` +} + +func (HandleTest) JSONSchemaHandle(handle ReflectorHandle) *Schema { + schema := &Schema{ + OneOf: []*Schema{ + handle.SchemaFor(TypeOne{}), + handle.SchemaForType(reflect.TypeOf(TypeTwo{})), + handle.SchemaFor(RecursiveType{}), + }, + } + return schema +} + func TestReflector(t *testing.T) { r := new(Reflector) s := "http://example.com/schema" @@ -470,6 +495,7 @@ func TestSchemaGeneration(t *testing.T) { {SchemaExtendTest{}, &Reflector{}, "fixtures/custom_type_extend.json"}, {Expression{}, &Reflector{}, "fixtures/schema_with_expression.json"}, {PatternEqualsTest{}, &Reflector{}, "fixtures/equals_in_pattern.json"}, + {HandleTest{}, &Reflector{}, "fixtures/reflect_with_handle.json"}, } for _, tt := range tests { @@ -497,8 +523,6 @@ func TestBaselineUnmarshal(t *testing.T) { func compareSchemaOutput(t *testing.T, f string, r *Reflector, obj any) { t.Helper() - expectedJSON, err := os.ReadFile(f) - require.NoError(t, err) actualSchema := r.Reflect(obj) actualJSON, _ := json.MarshalIndent(actualSchema, "", " ") //nolint:errchkjson @@ -507,6 +531,9 @@ func compareSchemaOutput(t *testing.T, f string, r *Reflector, obj any) { _ = os.WriteFile(f, actualJSON, 0600) } + expectedJSON, err := os.ReadFile(f) + require.NoError(t, err) + if !assert.JSONEq(t, string(expectedJSON), string(actualJSON)) { if *compareFixtures { _ = os.WriteFile(strings.TrimSuffix(f, ".json")+".out.json", actualJSON, 0600) diff --git a/schema.go b/schema.go index 2d914b8..3a4a0d6 100644 --- a/schema.go +++ b/schema.go @@ -35,10 +35,11 @@ type Schema struct { Items *Schema `json:"items,omitempty"` // section 10.3.1.2 (replaces additionalItems) Contains *Schema `json:"contains,omitempty"` // section 10.3.1.3 // RFC draft-bhutton-json-schema-00 section 10.3.2 (sub-schemas) - Properties *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"` // section 10.3.2.1 - PatternProperties map[string]*Schema `json:"patternProperties,omitempty"` // section 10.3.2.2 - AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // section 10.3.2.3 - PropertyNames *Schema `json:"propertyNames,omitempty"` // section 10.3.2.4 + Properties *orderedmap.OrderedMap[string, *Schema] `json:"properties,omitempty"` // section 10.3.2.1 + PatternProperties map[string]*Schema `json:"patternProperties,omitempty"` // section 10.3.2.2 + AdditionalProperties *Schema `json:"additionalProperties,omitempty"` // section 10.3.2.3 + UnevaluatedProperties *Schema `json:"unevaluatedProperties,omitempty"` // section 11.3 + PropertyNames *Schema `json:"propertyNames,omitempty"` // section 10.3.2.4 // RFC draft-bhutton-json-schema-validation-00, section 6 Type string `json:"type,omitempty"` // section 6.1.1 Enum []any `json:"enum,omitempty"` // section 6.1.2