Skip to content

Commit

Permalink
Merge pull request #108 from invopop/schema-property-alias
Browse files Browse the repository at this point in the history
Support for JSONSchemaAlias method
  • Loading branch information
samlown committed Oct 4, 2023
2 parents 12cbc49 + f45517c commit 9b6bb6e
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 92 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,9 +304,14 @@ 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 four 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() any` - is called when reflecting the type of object and allows for an alternative to be used instead.
- `JSONSchemaProperty(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:

Expand Down
19 changes: 19 additions & 0 deletions fixtures/schema_alias.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/invopop/jsonschema/alias-object-b",
"$ref": "#/$defs/AliasObjectA",
"$defs": {
"AliasObjectA": {
"properties": {
"prop_a": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"prop_a"
]
}
}
}
31 changes: 31 additions & 0 deletions fixtures/schema_alias_2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/invopop/jsonschema/alias-object-c",
"$ref": "#/$defs/AliasObjectC",
"$defs": {
"AliasObjectA": {
"properties": {
"prop_a": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"prop_a"
]
},
"AliasObjectC": {
"properties": {
"obj_b": {
"$ref": "#/$defs/AliasObjectA"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"obj_b"
]
}
}
}
31 changes: 31 additions & 0 deletions fixtures/schema_property_alias.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://github.com/invopop/jsonschema/alias-property-object-base",
"$ref": "#/$defs/AliasPropertyObjectBase",
"$defs": {
"AliasObjectA": {
"properties": {
"prop_a": {
"type": "string"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"prop_a"
]
},
"AliasPropertyObjectBase": {
"properties": {
"object": {
"$ref": "#/$defs/AliasObjectA"
}
},
"additionalProperties": false,
"type": "object",
"required": [
"object"
]
}
}
}
133 changes: 43 additions & 90 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -114,6 +30,22 @@ type extendSchemaImpl interface {
JSONSchemaExtend(*Schema)
}

// If the object to be reflected defines a `JSONSchemaAlias` method, its type will
// be used instead of the original type.
type aliasSchemaImpl interface {
JSONSchemaAlias() any
}

// If an object to be reflected defines a `JSONSchemaPropertyAlias` method,
// it will be called for each property to determine if another object
// should be used for the contents.
type propertyAliasSchemaImpl interface {
JSONSchemaProperty(prop string) any
}

var customAliasSchema = reflect.TypeOf((*aliasSchemaImpl)(nil)).Elem()
var customPropertyAliasSchema = reflect.TypeOf((*propertyAliasSchemaImpl)(nil)).Elem()

var customType = reflect.TypeOf((*customSchemaImpl)(nil)).Elem()
var extendType = reflect.TypeOf((*extendSchemaImpl)(nil)).Elem()

Expand Down Expand Up @@ -275,11 +207,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 (
Expand Down Expand Up @@ -342,6 +269,15 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
return r.refOrReflectTypeToSchema(definitions, t.Elem())
}

// Check if the there is an alias method that provides an object
// that we should use instead of this one.
if t.Implements(customAliasSchema) {
v := reflect.New(t)
o := v.Interface().(aliasSchemaImpl)
t = reflect.TypeOf(o.JSONSchemaAlias())
return r.refOrReflectTypeToSchema(definitions, t)
}

// Do any pre-definitions exist?
if r.Mapper != nil {
if t := r.Mapper(t); t != nil {
Expand Down Expand Up @@ -546,6 +482,15 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r
getFieldDocString = o.GetFieldDocString
}

customPropertyMethod := func(string) any {
return nil
}
if t.Implements(customPropertyAliasSchema) {
v := reflect.New(t)
o := v.Interface().(propertyAliasSchemaImpl)
customPropertyMethod = o.JSONSchemaProperty
}

handleField := func(f reflect.StructField) {
name, shouldEmbed, required, nullable := r.reflectFieldName(f)
// if anonymous and exported type should be processed recursively
Expand All @@ -557,7 +502,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 := customPropertyMethod(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)
Expand Down
35 changes: 35 additions & 0 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,38 @@ 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 AliasObjectC struct {
ObjB *AliasObjectB `json:"obj_b"`
}
type AliasPropertyObjectBase struct {
Object any `json:"object"`
}

func (AliasPropertyObjectBase) JSONSchemaProperty(prop string) any {
if prop == "object" {
return &AliasObjectA{}
}
return nil
}

func (AliasObjectB) JSONSchemaAlias() any {
return AliasObjectA{}
}

func TestJSONSchemaProperty(t *testing.T) {
r := &Reflector{}
compareSchemaOutput(t, "fixtures/schema_property_alias.json", r, &AliasPropertyObjectBase{})
}

func TestJSONSchemaAlias(t *testing.T) {
r := &Reflector{}
compareSchemaOutput(t, "fixtures/schema_alias.json", r, &AliasObjectB{})
compareSchemaOutput(t, "fixtures/schema_alias_2.json", r, &AliasObjectC{})
}

0 comments on commit 9b6bb6e

Please sign in to comment.