Skip to content

Commit

Permalink
Support for JSONSchemaAlias method
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Oct 4, 2023
1 parent 12cbc49 commit 0108689
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 92 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
31 changes: 31 additions & 0 deletions fixtures/schema_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-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"
]
}
}
}
117 changes: 27 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,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()

Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
23 changes: 23 additions & 0 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Check failure on line 637 in reflect_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

singleCaseSwitch: should rewrite switch statement to if statement (gocritic)
case "object":
return &AliasObjectB{}
}
return nil
}

func TestJSONSchemaAlias(t *testing.T) {
r := &Reflector{}
compareSchemaOutput(t, "fixtures/schema_alias.json", r, &AliasObjectBase{})
}
94 changes: 94 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 0108689

Please sign in to comment.