From 0108689b9f4e04f64a5986ea21650f07673c21cf Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 4 Oct 2023 14:42:55 +0000 Subject: [PATCH] Support for JSONSchemaAlias method --- README.md | 8 ++- fixtures/schema_alias.json | 31 ++++++++++ reflect.go | 117 +++++++++---------------------------- reflect_test.go | 23 ++++++++ schema.go | 94 +++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+), 92 deletions(-) create mode 100644 fixtures/schema_alias.json create mode 100644 schema.go diff --git a/README.md b/README.md index 65ed8ae..92c8a62 100644 --- a/README.md +++ b/README.md @@ -304,9 +304,13 @@ As you can see, if a field name has a `json:""` tag set, the `key` argument to ` Sometimes it can be useful to have custom JSON Marshal and Unmarshal methods in your structs that automatically convert for example a string into an object. -To override auto-generating an object type for your type, implement the `JSONSchema() *Schema` method and whatever is defined will be provided in the schema definitions. +This library will recognize and attempt to call three different methods that help you adjust schemas to your specific needs: -You also have the option of defining a `JSONSchemaExtend(schema *jsonschema.Schema)` method for your types that will be called _after_ the schema has been generated, allowing you to add or manipulate the fields easily. +- `JSONSchema() *Schema` - will prevent auto-generation of the schema so that you can provide your own definition. +- `JSONSchemaExtend(schema *jsonschema.Schema)` - will be called _after_ the schema has been generated, allowing you to add or manipulate the fields easily. +- `JSONSchemaAlias(prop string) any` - will be called for every property inside a struct giving you the chance to provide an alternative object to convert into a schema. + +Note that all of these methods **must** be defined on a non-pointer object for them to be called. Take the following simplified example of a `CompactDate` that only includes the Year and Month: diff --git a/fixtures/schema_alias.json b/fixtures/schema_alias.json new file mode 100644 index 0000000..2f921db --- /dev/null +++ b/fixtures/schema_alias.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/invopop/jsonschema/alias-object-base", + "$ref": "#/$defs/AliasObjectBase", + "$defs": { + "AliasObjectB": { + "properties": { + "prop_b": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "prop_b" + ] + }, + "AliasObjectBase": { + "properties": { + "object": { + "$ref": "#/$defs/AliasObjectB" + } + }, + "additionalProperties": false, + "type": "object", + "required": [ + "object" + ] + } + } +} \ No newline at end of file diff --git a/reflect.go b/reflect.go index b9f35f4..4a6d7bb 100644 --- a/reflect.go +++ b/reflect.go @@ -15,90 +15,6 @@ import ( "strconv" "strings" "time" - - orderedmap "github.com/wk8/go-ordered-map/v2" -) - -// Version is the JSON Schema version. -var Version = "https://json-schema.org/draft/2020-12/schema" - -// Schema represents a JSON Schema object type. -// RFC draft-bhutton-json-schema-00 section 4.3 -type Schema struct { - // RFC draft-bhutton-json-schema-00 - Version string `json:"$schema,omitempty"` // section 8.1.1 - ID ID `json:"$id,omitempty"` // section 8.2.1 - Anchor string `json:"$anchor,omitempty"` // section 8.2.2 - Ref string `json:"$ref,omitempty"` // section 8.2.3.1 - DynamicRef string `json:"$dynamicRef,omitempty"` // section 8.2.3.2 - Definitions Definitions `json:"$defs,omitempty"` // section 8.2.4 - Comments string `json:"$comment,omitempty"` // section 8.3 - // RFC draft-bhutton-json-schema-00 section 10.2.1 (Sub-schemas with logic) - AllOf []*Schema `json:"allOf,omitempty"` // section 10.2.1.1 - AnyOf []*Schema `json:"anyOf,omitempty"` // section 10.2.1.2 - OneOf []*Schema `json:"oneOf,omitempty"` // section 10.2.1.3 - Not *Schema `json:"not,omitempty"` // section 10.2.1.4 - // RFC draft-bhutton-json-schema-00 section 10.2.2 (Apply sub-schemas conditionally) - If *Schema `json:"if,omitempty"` // section 10.2.2.1 - Then *Schema `json:"then,omitempty"` // section 10.2.2.2 - Else *Schema `json:"else,omitempty"` // section 10.2.2.3 - DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"` // section 10.2.2.4 - // RFC draft-bhutton-json-schema-00 section 10.3.1 (arrays) - PrefixItems []*Schema `json:"prefixItems,omitempty"` // section 10.3.1.1 - 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 - // 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 - Const any `json:"const,omitempty"` // section 6.1.3 - MultipleOf json.Number `json:"multipleOf,omitempty"` // section 6.2.1 - Maximum json.Number `json:"maximum,omitempty"` // section 6.2.2 - ExclusiveMaximum json.Number `json:"exclusiveMaximum,omitempty"` // section 6.2.3 - Minimum json.Number `json:"minimum,omitempty"` // section 6.2.4 - ExclusiveMinimum json.Number `json:"exclusiveMinimum,omitempty"` // section 6.2.5 - MaxLength *uint64 `json:"maxLength,omitempty"` // section 6.3.1 - MinLength *uint64 `json:"minLength,omitempty"` // section 6.3.2 - Pattern string `json:"pattern,omitempty"` // section 6.3.3 - MaxItems *uint64 `json:"maxItems,omitempty"` // section 6.4.1 - MinItems *uint64 `json:"minItems,omitempty"` // section 6.4.2 - UniqueItems bool `json:"uniqueItems,omitempty"` // section 6.4.3 - MaxContains *uint64 `json:"maxContains,omitempty"` // section 6.4.4 - MinContains *uint64 `json:"minContains,omitempty"` // section 6.4.5 - MaxProperties *uint64 `json:"maxProperties,omitempty"` // section 6.5.1 - MinProperties *uint64 `json:"minProperties,omitempty"` // section 6.5.2 - Required []string `json:"required,omitempty"` // section 6.5.3 - DependentRequired map[string][]string `json:"dependentRequired,omitempty"` // section 6.5.4 - // RFC draft-bhutton-json-schema-validation-00, section 7 - Format string `json:"format,omitempty"` - // RFC draft-bhutton-json-schema-validation-00, section 8 - ContentEncoding string `json:"contentEncoding,omitempty"` // section 8.3 - ContentMediaType string `json:"contentMediaType,omitempty"` // section 8.4 - ContentSchema *Schema `json:"contentSchema,omitempty"` // section 8.5 - // RFC draft-bhutton-json-schema-validation-00, section 9 - Title string `json:"title,omitempty"` // section 9.1 - Description string `json:"description,omitempty"` // section 9.1 - Default any `json:"default,omitempty"` // section 9.2 - Deprecated bool `json:"deprecated,omitempty"` // section 9.3 - ReadOnly bool `json:"readOnly,omitempty"` // section 9.4 - WriteOnly bool `json:"writeOnly,omitempty"` // section 9.4 - Examples []any `json:"examples,omitempty"` // section 9.5 - - Extras map[string]any `json:"-"` - - // Special boolean representation of the Schema - section 4.3.2 - boolean *bool -} - -var ( - // TrueSchema defines a schema with a true value - TrueSchema = &Schema{boolean: &[]bool{true}[0]} - // FalseSchema defines a schema with a false value - FalseSchema = &Schema{boolean: &[]bool{false}[0]} ) // customSchemaImpl is used to detect if the type provides it's own @@ -114,6 +30,15 @@ type extendSchemaImpl interface { JSONSchemaExtend(*Schema) } +// If an object to be reflected defines a `JSONSchemaAlias` method, +// it will be called for each property to determine if another object +// should be used for the contents. +type aliasSchemaImpl interface { + JSONSchemaAlias(prop string) any +} + +var customAliasSchema = reflect.TypeOf((*aliasSchemaImpl)(nil)).Elem() + var customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem() var extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem() @@ -275,11 +200,6 @@ func (r *Reflector) ReflectFromType(t reflect.Type) *Schema { return s } -// Definitions hold schema definitions. -// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26 -// RFC draft-wright-json-schema-validation-00, section 5.26 -type Definitions map[string]*Schema - // Available Go defined types for JSON Schema Validation. // RFC draft-wright-json-schema-validation-00, section 7.3 var ( @@ -546,6 +466,15 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r getFieldDocString = o.GetFieldDocString } + customAliasMethod := func(string) any { + return nil + } + if t.Implements(customAliasSchema) { + v := reflect.New(t) + o := v.Interface().(aliasSchemaImpl) + customAliasMethod = o.JSONSchemaAlias + } + handleField := func(f reflect.StructField) { name, shouldEmbed, required, nullable := r.reflectFieldName(f) // if anonymous and exported type should be processed recursively @@ -557,7 +486,15 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r return } - property := r.refOrReflectTypeToSchema(definitions, f.Type) + // If a JSONSchemaAlias(prop string) method is defined, attempt to use + // the provided object's type instead of the field's type. + var property *Schema + if alias := customAliasMethod(name); alias != nil { + property = r.refOrReflectTypeToSchema(definitions, reflect.TypeOf(alias)) + } else { + property = r.refOrReflectTypeToSchema(definitions, f.Type) + } + property.structKeywordsFromTags(f, st, name) if property.Description == "" { property.Description = r.lookupComment(t, f.Name) diff --git a/reflect_test.go b/reflect_test.go index 6147c74..4174c84 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -622,3 +622,26 @@ func TestUnsignedIntHandling(t *testing.T) { fixtureContains(t, "fixtures/unsigned_int_handling.json", `"minItems": 0`) fixtureContains(t, "fixtures/unsigned_int_handling.json", `"maxItems": 0`) } + +type AliasObjectA struct { + PropA string `json:"prop_a"` +} +type AliasObjectB struct { + PropB string `json:"prop_b"` +} +type AliasObjectBase struct { + Object *AliasObjectA `json:"object"` +} + +func (AliasObjectBase) JSONSchemaAlias(prop string) any { + switch prop { + case "object": + return &AliasObjectB{} + } + return nil +} + +func TestJSONSchemaAlias(t *testing.T) { + r := &Reflector{} + compareSchemaOutput(t, "fixtures/schema_alias.json", r, &AliasObjectBase{}) +} diff --git a/schema.go b/schema.go new file mode 100644 index 0000000..2d914b8 --- /dev/null +++ b/schema.go @@ -0,0 +1,94 @@ +package jsonschema + +import ( + "encoding/json" + + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +// Version is the JSON Schema version. +var Version = "https://json-schema.org/draft/2020-12/schema" + +// Schema represents a JSON Schema object type. +// RFC draft-bhutton-json-schema-00 section 4.3 +type Schema struct { + // RFC draft-bhutton-json-schema-00 + Version string `json:"$schema,omitempty"` // section 8.1.1 + ID ID `json:"$id,omitempty"` // section 8.2.1 + Anchor string `json:"$anchor,omitempty"` // section 8.2.2 + Ref string `json:"$ref,omitempty"` // section 8.2.3.1 + DynamicRef string `json:"$dynamicRef,omitempty"` // section 8.2.3.2 + Definitions Definitions `json:"$defs,omitempty"` // section 8.2.4 + Comments string `json:"$comment,omitempty"` // section 8.3 + // RFC draft-bhutton-json-schema-00 section 10.2.1 (Sub-schemas with logic) + AllOf []*Schema `json:"allOf,omitempty"` // section 10.2.1.1 + AnyOf []*Schema `json:"anyOf,omitempty"` // section 10.2.1.2 + OneOf []*Schema `json:"oneOf,omitempty"` // section 10.2.1.3 + Not *Schema `json:"not,omitempty"` // section 10.2.1.4 + // RFC draft-bhutton-json-schema-00 section 10.2.2 (Apply sub-schemas conditionally) + If *Schema `json:"if,omitempty"` // section 10.2.2.1 + Then *Schema `json:"then,omitempty"` // section 10.2.2.2 + Else *Schema `json:"else,omitempty"` // section 10.2.2.3 + DependentSchemas map[string]*Schema `json:"dependentSchemas,omitempty"` // section 10.2.2.4 + // RFC draft-bhutton-json-schema-00 section 10.3.1 (arrays) + PrefixItems []*Schema `json:"prefixItems,omitempty"` // section 10.3.1.1 + 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 + // 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 + Const any `json:"const,omitempty"` // section 6.1.3 + MultipleOf json.Number `json:"multipleOf,omitempty"` // section 6.2.1 + Maximum json.Number `json:"maximum,omitempty"` // section 6.2.2 + ExclusiveMaximum json.Number `json:"exclusiveMaximum,omitempty"` // section 6.2.3 + Minimum json.Number `json:"minimum,omitempty"` // section 6.2.4 + ExclusiveMinimum json.Number `json:"exclusiveMinimum,omitempty"` // section 6.2.5 + MaxLength *uint64 `json:"maxLength,omitempty"` // section 6.3.1 + MinLength *uint64 `json:"minLength,omitempty"` // section 6.3.2 + Pattern string `json:"pattern,omitempty"` // section 6.3.3 + MaxItems *uint64 `json:"maxItems,omitempty"` // section 6.4.1 + MinItems *uint64 `json:"minItems,omitempty"` // section 6.4.2 + UniqueItems bool `json:"uniqueItems,omitempty"` // section 6.4.3 + MaxContains *uint64 `json:"maxContains,omitempty"` // section 6.4.4 + MinContains *uint64 `json:"minContains,omitempty"` // section 6.4.5 + MaxProperties *uint64 `json:"maxProperties,omitempty"` // section 6.5.1 + MinProperties *uint64 `json:"minProperties,omitempty"` // section 6.5.2 + Required []string `json:"required,omitempty"` // section 6.5.3 + DependentRequired map[string][]string `json:"dependentRequired,omitempty"` // section 6.5.4 + // RFC draft-bhutton-json-schema-validation-00, section 7 + Format string `json:"format,omitempty"` + // RFC draft-bhutton-json-schema-validation-00, section 8 + ContentEncoding string `json:"contentEncoding,omitempty"` // section 8.3 + ContentMediaType string `json:"contentMediaType,omitempty"` // section 8.4 + ContentSchema *Schema `json:"contentSchema,omitempty"` // section 8.5 + // RFC draft-bhutton-json-schema-validation-00, section 9 + Title string `json:"title,omitempty"` // section 9.1 + Description string `json:"description,omitempty"` // section 9.1 + Default any `json:"default,omitempty"` // section 9.2 + Deprecated bool `json:"deprecated,omitempty"` // section 9.3 + ReadOnly bool `json:"readOnly,omitempty"` // section 9.4 + WriteOnly bool `json:"writeOnly,omitempty"` // section 9.4 + Examples []any `json:"examples,omitempty"` // section 9.5 + + Extras map[string]any `json:"-"` + + // Special boolean representation of the Schema - section 4.3.2 + boolean *bool +} + +var ( + // TrueSchema defines a schema with a true value + TrueSchema = &Schema{boolean: &[]bool{true}[0]} + // FalseSchema defines a schema with a false value + FalseSchema = &Schema{boolean: &[]bool{false}[0]} +) + +// Definitions hold schema definitions. +// http://json-schema.org/latest/json-schema-validation.html#rfc.section.5.26 +// RFC draft-wright-json-schema-validation-00, section 5.26 +type Definitions map[string]*Schema