1package jsonschema 2 3import ( 4 "encoding/json" 5 "io/ioutil" 6 "net" 7 "net/url" 8 "path/filepath" 9 "reflect" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/stretchr/testify/require" 15) 16 17type GrandfatherType struct { 18 FamilyName string `json:"family_name" jsonschema:"required"` 19} 20 21type SomeBaseType struct { 22 SomeBaseProperty int `json:"some_base_property"` 23 SomeBasePropertyYaml int `yaml:"some_base_property_yaml"` 24 // The jsonschema required tag is nonsensical for private and ignored properties. 25 // Their presence here tests that the fields *will not* be required in the output 26 // schema, even if they are tagged required. 27 somePrivateBaseProperty string `json:"i_am_private" jsonschema:"required"` 28 SomeIgnoredBaseProperty string `json:"-" jsonschema:"required"` 29 SomeSchemaIgnoredProperty string `jsonschema:"-,required"` 30 Grandfather GrandfatherType `json:"grand"` 31 32 SomeUntaggedBaseProperty bool `jsonschema:"required"` 33 someUnexportedUntaggedBaseProperty bool 34} 35 36type MapType map[string]interface{} 37 38type nonExported struct { 39 PublicNonExported int 40 privateNonExported int 41} 42 43type ProtoEnum int32 44 45func (ProtoEnum) EnumDescriptor() ([]byte, []int) { return []byte(nil), []int{0} } 46 47const ( 48 Unset ProtoEnum = iota 49 Great 50) 51 52type TestUser struct { 53 SomeBaseType 54 nonExported 55 MapType 56 57 ID int `json:"id" jsonschema:"required"` 58 Name string `json:"name" jsonschema:"required,minLength=1,maxLength=20,pattern=.*,description=this is a property,title=the name,example=joe,example=lucy,default=alex"` 59 Friends []int `json:"friends,omitempty" jsonschema_description:"list of IDs, omitted when empty"` 60 Tags map[string]interface{} `json:"tags,omitempty"` 61 62 TestFlag bool 63 IgnoredCounter int `json:"-"` 64 65 // Tests for RFC draft-wright-json-schema-validation-00, section 7.3 66 BirthDate time.Time `json:"birth_date,omitempty"` 67 Website url.URL `json:"website,omitempty"` 68 IPAddress net.IP `json:"network_address,omitempty"` 69 70 // Tests for RFC draft-wright-json-schema-hyperschema-00, section 4 71 Photo []byte `json:"photo,omitempty" jsonschema:"required"` 72 73 // Tests for jsonpb enum support 74 Feeling ProtoEnum `json:"feeling,omitempty"` 75 Age int `json:"age" jsonschema:"minimum=18,maximum=120,exclusiveMaximum=true,exclusiveMinimum=true"` 76 Email string `json:"email" jsonschema:"format=email"` 77 78 // Test for "extras" support 79 Baz string `jsonschema_extras:"foo=bar,hello=world,foo=bar1"` 80 81 // Tests for simple enum tags 82 Color string `json:"color" jsonschema:"enum=red,enum=green,enum=blue"` 83 Rank int `json:"rank,omitempty" jsonschema:"enum=1,enum=2,enum=3"` 84 Multiplier float64 `json:"mult,omitempty" jsonschema:"enum=1.0,enum=1.5,enum=2.0"` 85} 86 87type CustomTime time.Time 88 89type CustomTypeField struct { 90 CreatedAt CustomTime 91} 92 93type RootOneOf struct { 94 Field1 string `json:"field1" jsonschema:"oneof_required=group1"` 95 Field2 string `json:"field2" jsonschema:"oneof_required=group2"` 96 Field3 interface{} `json:"field3" jsonschema:"oneof_type=string;array"` 97 Field4 string `json:"field4" jsonschema:"oneof_required=group1"` 98 Field5 ChildOneOf `json:"child"` 99} 100 101type ChildOneOf struct { 102 Child1 string `json:"child1" jsonschema:"oneof_required=group1"` 103 Child2 string `json:"child2" jsonschema:"oneof_required=group2"` 104 Child3 interface{} `json:"child3" jsonschema:"oneof_required=group2,oneof_type=string;array"` 105 Child4 string `json:"child4" jsonschema:"oneof_required=group1"` 106} 107 108func TestSchemaGeneration(t *testing.T) { 109 tests := []struct { 110 typ interface{} 111 reflector *Reflector 112 fixture string 113 }{ 114 {&RootOneOf{}, &Reflector{RequiredFromJSONSchemaTags: true}, "fixtures/oneof.json"}, 115 {&TestUser{}, &Reflector{}, "fixtures/defaults.json"}, 116 {&TestUser{}, &Reflector{AllowAdditionalProperties: true}, "fixtures/allow_additional_props.json"}, 117 {&TestUser{}, &Reflector{RequiredFromJSONSchemaTags: true}, "fixtures/required_from_jsontags.json"}, 118 {&TestUser{}, &Reflector{ExpandedStruct: true}, "fixtures/defaults_expanded_toplevel.json"}, 119 {&TestUser{}, &Reflector{IgnoredTypes: []interface{}{GrandfatherType{}}}, "fixtures/ignore_type.json"}, 120 {&TestUser{}, &Reflector{DoNotReference: true}, "fixtures/no_reference.json"}, 121 {&TestUser{}, &Reflector{FullyQualifyTypeNames: true}, "fixtures/fully_qualified.json"}, 122 {&TestUser{}, &Reflector{DoNotReference: true, FullyQualifyTypeNames: true}, "fixtures/no_ref_qual_types.json"}, 123 {&CustomTypeField{}, &Reflector{ 124 TypeMapper: func(i reflect.Type) *Type { 125 if i == reflect.TypeOf(CustomTime{}) { 126 return &Type{ 127 Type: "string", 128 Format: "date-time", 129 } 130 } 131 return nil 132 }, 133 }, "fixtures/custom_type.json"}, 134 } 135 136 for _, tt := range tests { 137 name := strings.TrimSuffix(filepath.Base(tt.fixture), ".json") 138 t.Run(name, func(t *testing.T) { 139 f, err := ioutil.ReadFile(tt.fixture) 140 require.NoError(t, err) 141 142 actualSchema := tt.reflector.Reflect(tt.typ) 143 expectedSchema := &Schema{} 144 145 err = json.Unmarshal(f, expectedSchema) 146 require.NoError(t, err) 147 148 expectedJSON, _ := json.MarshalIndent(expectedSchema, "", " ") 149 actualJSON, _ := json.MarshalIndent(actualSchema, "", " ") 150 require.Equal(t, string(expectedJSON), string(actualJSON)) 151 }) 152 } 153} 154 155func TestBaselineUnmarshal(t *testing.T) { 156 expectedJSON, err := ioutil.ReadFile("fixtures/defaults.json") 157 require.NoError(t, err) 158 159 reflector := &Reflector{} 160 actualSchema := reflector.Reflect(&TestUser{}) 161 162 actualJSON, _ := json.MarshalIndent(actualSchema, "", " ") 163 164 require.Equal(t, strings.Replace(string(expectedJSON), `\/`, "/", -1), string(actualJSON)) 165} 166