Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions fixtures/multivalued_type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"properties": {
"other": {
"type": "string"
},
"withdrawals": {
"type": [
"array",
"null"
]
}
},
"required": [
"withdrawals"
],
"description": "test of Schema with multivalued property Type"
}
58 changes: 29 additions & 29 deletions reflect.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,8 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
// It will unmarshal either.
if t.Implements(protoEnumType) {
st.OneOf = []*Schema{
{Type: "string"},
{Type: "integer"},
{Type: TypeString},
{Type: TypeInteger},
}
return st
}
Expand All @@ -316,7 +316,7 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)
// TODO email RFC section 7.3.2, hostname RFC section 7.3.3, uriref RFC section 7.3.7
if t == ipType {
// TODO differentiate ipv4 and ipv6 RFC section 7.3.4, 7.3.5
st.Type = "string"
st.Type = TypeString
st.Format = "ipv4"
return st
}
Expand All @@ -336,16 +336,16 @@ func (r *Reflector) reflectTypeToSchema(definitions Definitions, t reflect.Type)

case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
st.Type = "integer"
st.Type = TypeInteger

case reflect.Float32, reflect.Float64:
st.Type = "number"
st.Type = TypeNumber

case reflect.Bool:
st.Type = "boolean"
st.Type = TypeBoolean

case reflect.String:
st.Type = "string"
st.Type = TypeString

default:
panic("unsupported type " + t.String())
Expand Down Expand Up @@ -410,19 +410,19 @@ func (r *Reflector) reflectSliceOrArray(definitions Definitions, t reflect.Type,
st.MaxItems = &l
}
if t.Kind() == reflect.Slice && t.Elem() == byteSliceType.Elem() {
st.Type = "string"
st.Type = TypeString
// NOTE: ContentMediaType is not set here
st.ContentEncoding = "base64"
} else {
st.Type = "array"
st.Type = TypeArray
st.Items = r.refOrReflectTypeToSchema(definitions, t.Elem())
}
}

func (r *Reflector) reflectMap(definitions Definitions, t reflect.Type, st *Schema) {
r.addDefinition(definitions, t, st)

st.Type = "object"
st.Type = TypeObject
if st.Description == "" {
st.Description = r.lookupComment(t, "")
}
Expand All @@ -445,17 +445,17 @@ func (r *Reflector) reflectStruct(definitions Definitions, t reflect.Type, s *Sc
// Handle special types
switch t {
case timeType: // date-time RFC section 7.3.1
s.Type = "string"
s.Type = TypeString
s.Format = "date-time"
return
case uriType: // uri RFC section 7.3.6
s.Type = "string"
s.Type = TypeString
s.Format = "uri"
return
}

r.addDefinition(definitions, t, s)
s.Type = "object"
s.Type = TypeObject
s.Properties = NewProperties()
s.Description = r.lookupComment(t, "")
if r.AssignAnchor {
Expand Down Expand Up @@ -534,7 +534,7 @@ func (r *Reflector) reflectStructFields(st *Schema, definitions Definitions, t r
OneOf: []*Schema{
property,
{
Type: "null",
Type: TypeNull,
},
},
}
Expand Down Expand Up @@ -612,15 +612,15 @@ func (t *Schema) structKeywordsFromTags(f reflect.StructField, parent *Schema, p
tags = t.genericKeywords(tags, parent, propertyName)

switch t.Type {
case "string":
case TypeString:
t.stringKeywords(tags)
case "number":
case TypeNumber:
t.numericalKeywords(tags)
case "integer":
case TypeInteger:
t.numericalKeywords(tags)
case "array":
case TypeArray:
t.arrayKeywords(tags)
case "boolean":
case TypeBoolean:
t.booleanKeywords(tags)
}
extras := strings.Split(f.Tag.Get("jsonschema_extras"), ",")
Expand All @@ -640,7 +640,7 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str
case "description":
t.Description = val
case "type":
t.Type = val
t.Type = Type(val)
case "anchor":
t.Anchor = val
case "oneof_required":
Expand Down Expand Up @@ -692,11 +692,11 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str
if t.OneOf == nil {
t.OneOf = make([]*Schema, 0, 1)
}
t.Type = ""
t.Type = typeUndefined
types := strings.Split(nameValue[1], ";")
for _, ty := range types {
t.OneOf = append(t.OneOf, &Schema{
Type: ty,
Type: Type(ty),
})
}
case "anyof_ref":
Expand All @@ -718,11 +718,11 @@ func (t *Schema) genericKeywords(tags []string, parent *Schema, propertyName str
if t.AnyOf == nil {
t.AnyOf = make([]*Schema, 0, 1)
}
t.Type = ""
t.Type = typeUndefined
types := strings.Split(nameValue[1], ";")
for _, ty := range types {
t.AnyOf = append(t.AnyOf, &Schema{
Type: ty,
Type: Type(ty),
})
}
default:
Expand Down Expand Up @@ -870,15 +870,15 @@ func (t *Schema) arrayKeywords(tags []string) {
}

switch t.Items.Type {
case "string":
case TypeString:
t.Items.stringKeywords(unprocessed)
case "number":
case TypeNumber:
t.Items.numericalKeywords(unprocessed)
case "integer":
case TypeInteger:
t.Items.numericalKeywords(unprocessed)
case "array":
case TypeArray:
// explicitly don't support traversal for the [][]..., as it's unclear where the array tags belong
case "boolean":
case TypeBoolean:
t.Items.booleanKeywords(unprocessed)
}
}
Expand Down
20 changes: 10 additions & 10 deletions reflect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ type CustomTypeFieldWithInterface struct {

func (CustomTimeWithInterface) JSONSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
Format: "date-time",
}
}
Expand Down Expand Up @@ -208,7 +208,7 @@ type UserWithAnchor struct {

func (CompactDate) JSONSchema() *Schema {
return &Schema{
Type: "string",
Type: TypeString,
Title: "Compact Date",
Description: "Short date that only includes year and month",
Pattern: "^[0-9]{4}-[0-1][0-9]$",
Expand Down Expand Up @@ -256,11 +256,11 @@ type CustomSliceType []string
func (CustomSliceType) JSONSchema() *Schema {
return &Schema{
OneOf: []*Schema{{
Type: "string",
Type: TypeString,
}, {
Type: "array",
Type: TypeArray,
Items: &Schema{
Type: "string",
Type: TypeString,
},
}},
}
Expand All @@ -271,15 +271,15 @@ type CustomMapType map[string]string
func (CustomMapType) JSONSchema() *Schema {
properties := NewProperties()
properties.Set("key", &Schema{
Type: "string",
Type: TypeString,
})
properties.Set("value", &Schema{
Type: "string",
Type: TypeString,
})
return &Schema{
Type: "array",
Type: TypeArray,
Items: &Schema{
Type: "object",
Type: TypeObject,
Properties: properties,
Required: []string{"key", "value"},
},
Expand Down Expand Up @@ -386,7 +386,7 @@ func TestSchemaGeneration(t *testing.T) {
Mapper: func(i reflect.Type) *Schema {
if i == reflect.TypeOf(CustomTime{}) {
return &Schema{
Type: "string",
Type: TypeString,
Format: "date-time",
}
}
Expand Down
73 changes: 72 additions & 1 deletion schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package jsonschema

import (
"encoding/json"
"strings"

orderedmap "github.com/wk8/go-ordered-map/v2"
)
Expand Down Expand Up @@ -40,7 +41,7 @@ type Schema struct {
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
Type Type `json:"type,omitempty"` // section 6.1.1

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing Schema.Type from string to the new Type defined type is a compile-time breaking API change for callers that previously assigned a string variable (not a string literal) to Schema.Type. If the intent is to remain source-compatible in v1, consider an approach that keeps the public field as string (or adds a new field) and uses custom marshal/unmarshal to support both string and []string in JSON, or clearly document this as a breaking change requiring a major version bump.

Copilot uses AI. Check for mistakes.
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
Expand Down Expand Up @@ -92,3 +93,73 @@ var (
// 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

// Type represents the property types allowed by the JSONSchema specification,s

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "specification,s" looks like it should be "specification's" (or just "specification").

Suggested change
// Type represents the property types allowed by the JSONSchema specification,s
// Type represents the property types allowed by the JSONSchema specification's

Copilot uses AI. Check for mistakes.
// [instance data model].
//
// Constants are provided for each of the types supported by this library's
// vocabulary. Types that can be one of several values are supported by
// concatenating the constant values below with a comma separator. The
// MarshalJSON and UnmarshalJSON functions handle the multivalued Type
// fields properly but care must be taken when treating a Type as a string.
//
// [instance data model]: https://json-schema.org/draft/2020-12/json-schema-core#name-instance-data-model
type Type string

// The Type constants below represent the six standard JSON types as well as
// TypeInteger which is part of this library's extended vocabulary.
const (
typeUndefined Type = ""
TypeString Type = "string"
TypeInteger Type = "integer"
TypeNumber Type = "number"
TypeBoolean Type = "boolean"
TypeArray Type = "array"
TypeObject Type = "object"
TypeNull Type = "null"
)

// NewMultivaluedType creates a Type that allows any of the provided types.
func NewMultivaluedType(types ...Type) Type {
var s []string
for _, t := range types {
s = append(s, string(t))
}

return Type(strings.Join(s, ","))
}

// IsMultivalued returns a boolean indicating whether the Type contains
// multiple underlying types.
func (t *Type) IsMultivalued() bool {
return strings.Contains(string(*t), ",")
}

// MarshalJSON implements json.Marshaler.
func (t *Type) MarshalJSON() ([]byte, error) {
if t.IsMultivalued() {
return json.Marshal(strings.Split(string(*t), ","))
}

return json.Marshal(string(*t))
Comment on lines +134 to +144

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Type methods use pointer receivers, which makes the API awkward to use: constants and temporary values (e.g. TypeArray.IsMultivalued() or NewMultivaluedType(...).IsMultivalued()) are not addressable, so these calls won’t compile. Consider switching IsMultivalued and MarshalJSON to value receivers (func (t Type) ...) while keeping UnmarshalJSON as a pointer receiver.

Suggested change
func (t *Type) IsMultivalued() bool {
return strings.Contains(string(*t), ",")
}
// MarshalJSON implements json.Marshaler.
func (t *Type) MarshalJSON() ([]byte, error) {
if t.IsMultivalued() {
return json.Marshal(strings.Split(string(*t), ","))
}
return json.Marshal(string(*t))
func (t Type) IsMultivalued() bool {
return strings.Contains(string(t), ",")
}
// MarshalJSON implements json.Marshaler.
func (t Type) MarshalJSON() ([]byte, error) {
if t.IsMultivalued() {
return json.Marshal(strings.Split(string(t), ","))
}
return json.Marshal(string(t))

Copilot uses AI. Check for mistakes.
}

// UnmarshalJSON implements json.Unmarshaler.
func (t *Type) UnmarshalJSON(data []byte) error {
var s string
err := json.Unmarshal(data, &s)
if err == nil {
*t = Type(s)

return nil
}

var a []string
if err := json.Unmarshal(data, &a); err != nil {
return err
}

*t = Type(strings.Join(a, ","))

return nil
}
Loading