diff --git a/.github/docs/openapi2.txt b/.github/docs/openapi2.txt index d65cd1815..328384100 100644 --- a/.github/docs/openapi2.txt +++ b/.github/docs/openapi2.txt @@ -51,7 +51,7 @@ type Parameter struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` + Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 7418add81..80909520a 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -19,6 +19,7 @@ const ( TypeNumber = "number" TypeObject = "object" TypeString = "string" + TypeNull = "null" ) const ( // FormatOfStringForUUIDOfRFC4122 is an optional predefined format for UUID v1-v5 as specified by RFC4122 @@ -1197,7 +1198,7 @@ type Schema struct { AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` + Type *Types `json:"type,omitempty" yaml:"type,omitempty"` Title string `json:"title,omitempty" yaml:"title,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -1299,6 +1300,8 @@ func (schema Schema) MarshalJSON() ([]byte, error) func (schema *Schema) NewRef() *SchemaRef +func (schema *Schema) PermitsNull() bool + func (schema *Schema) UnmarshalJSON(data []byte) error UnmarshalJSON sets Schema to a copy of data. @@ -1721,6 +1724,22 @@ func (tags Tags) Get(name string) *Tag func (tags Tags) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Tags does not comply with the OpenAPI spec. +type Types []string + +func (pTypes *Types) Includes(typ string) bool + +func (types *Types) Is(typ string) bool + +func (pTypes *Types) MarshalJSON() ([]byte, error) + +func (pTypes *Types) MarshalYAML() (interface{}, error) + +func (types *Types) Permits(typ string) bool + +func (types *Types) Slice() []string + +func (types *Types) UnmarshalJSON(data []byte) error + type ValidationOption func(options *ValidationOptions) ValidationOption allows the modification of how the OpenAPI document is validated. diff --git a/openapi2/parameter.go b/openapi2/parameter.go index 025509871..1203853f2 100644 --- a/openapi2/parameter.go +++ b/openapi2/parameter.go @@ -32,7 +32,7 @@ type Parameter struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` CollectionFormat string `json:"collectionFormat,omitempty" yaml:"collectionFormat,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` + Type *openapi3.Types `json:"type,omitempty" yaml:"type,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Pattern string `json:"pattern,omitempty" yaml:"pattern,omitempty"` AllowEmptyValue bool `json:"allowEmptyValue,omitempty" yaml:"allowEmptyValue,omitempty"` @@ -76,7 +76,7 @@ func (parameter Parameter) MarshalJSON() ([]byte, error) { if x := parameter.CollectionFormat; x != "" { m["collectionFormat"] = x } - if x := parameter.Type; x != "" { + if x := parameter.Type; x != nil { m["type"] = x } if x := parameter.Format; x != "" { diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go index 2404e9027..a79f72cd3 100644 --- a/openapi2conv/openapi2_conv.go +++ b/openapi2conv/openapi2_conv.go @@ -248,8 +248,8 @@ func ToV3Parameter(components *openapi3.Components, parameter *openapi2.Paramete case "formData": format, typ := parameter.Format, parameter.Type - if typ == "file" { - format, typ = "binary", "string" + if typ.Is("file") { + format, typ = "binary", &openapi3.Types{"string"} } if parameter.Extensions == nil { parameter.Extensions = make(map[string]interface{}, 1) @@ -347,7 +347,7 @@ func formDataBody(bodies map[string]*openapi3.SchemaRef, reqs map[string]bool, c } } schema := &openapi3.Schema{ - Type: "object", + Type: &openapi3.Types{"object"}, Properties: ToV3Schemas(bodies), Required: requireds, } @@ -772,8 +772,8 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components } if schema.Value != nil { - if schema.Value.Type == "string" && schema.Value.Format == "binary" { - paramType := "file" + if schema.Value.Type.Is("string") && schema.Value.Format == "binary" { + paramType := &openapi3.Types{"file"} required := false value, _ := schema.Value.Extensions["x-formData-name"] @@ -825,7 +825,7 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components for i, v := range schema.Value.AllOf { schema.Value.AllOf[i], _ = FromV3SchemaRef(v, components) } - if schema.Value.Nullable { + if schema.Value.PermitsNull() { schema.Value.Nullable = false if schema.Value.Extensions == nil { schema.Value.Extensions = make(map[string]interface{}) @@ -893,7 +893,7 @@ func FromV3RequestBodyFormData(mediaType *openapi3.MediaType) openapi2.Parameter val := schemaRef.Value typ := val.Type if val.Format == "binary" { - typ = "file" + typ = &openapi3.Types{"file"} } required := false for _, name := range val.Required { diff --git a/openapi3/issue301_test.go b/openapi3/issue301_test.go index ea14c3a76..cf5350d5f 100644 --- a/openapi3/issue301_test.go +++ b/openapi3/issue301_test.go @@ -16,7 +16,7 @@ func TestIssue301(t *testing.T) { err = doc.Validate(sl.Context) require.NoError(t, err) - require.Equal(t, "object", doc. + require.Equal(t, &Types{"object"}, doc. Paths.Value("/trans"). Post.Callbacks["transactionCallback"].Value. Value("http://notificationServer.com?transactionId={$request.body#/id}&email={$request.body#/email}"). @@ -24,7 +24,7 @@ func TestIssue301(t *testing.T) { Content["application/json"].Schema.Value. Type) - require.Equal(t, "boolean", doc. + require.Equal(t, &Types{"boolean"}, doc. Paths.Value("/other"). Post.Callbacks["myEvent"].Value. Value("{$request.query.queryUrl}"). diff --git a/openapi3/issue341_test.go b/openapi3/issue341_test.go index 2ceb45964..6154591e9 100644 --- a/openapi3/issue341_test.go +++ b/openapi3/issue341_test.go @@ -34,7 +34,7 @@ func TestIssue341(t *testing.T) { } }`, string(bs)) - require.Equal(t, "string", doc. + require.Equal(t, &Types{"string"}, doc. Paths.Value("/testpath"). Get. Responses.Value("200").Value. diff --git a/openapi3/issue344_test.go b/openapi3/issue344_test.go index 44ba2b7f5..03fc1b22d 100644 --- a/openapi3/issue344_test.go +++ b/openapi3/issue344_test.go @@ -16,5 +16,5 @@ func TestIssue344(t *testing.T) { err = doc.Validate(sl.Context) require.NoError(t, err) - require.Equal(t, "string", doc.Components.Schemas["Test"].Value.Properties["test"].Value.Properties["name"].Value.Type) + require.Equal(t, &Types{"string"}, doc.Components.Schemas["Test"].Value.Properties["test"].Value.Properties["name"].Value.Type) } diff --git a/openapi3/issue376_test.go b/openapi3/issue376_test.go index fd9286041..01c0a188c 100644 --- a/openapi3/issue376_test.go +++ b/openapi3/issue376_test.go @@ -40,7 +40,7 @@ info: require.Equal(t, 2, len(doc.Components.Schemas)) require.Equal(t, 0, doc.Paths.Len()) - require.Equal(t, "string", doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type) + require.Equal(t, &Types{"string"}, doc.Components.Schemas["schema2"].Value.Properties["prop"].Value.Type) } func TestExclusiveValuesOfValuesAdditionalProperties(t *testing.T) { diff --git a/openapi3/issue495_test.go b/openapi3/issue495_test.go index 0ed52df81..ee4cad50b 100644 --- a/openapi3/issue495_test.go +++ b/openapi3/issue495_test.go @@ -81,7 +81,7 @@ paths: err = doc.Validate(sl.Context) require.NoError(t, err) - require.Equal(t, &Schema{Type: "object"}, doc.Components.Schemas["schemaArray"].Value.Items.Value) + require.Equal(t, &Schema{Type: &Types{"object"}}, doc.Components.Schemas["schemaArray"].Value.Items.Value) } func TestIssue495WithDraft04(t *testing.T) { diff --git a/openapi3/issue638_test.go b/openapi3/issue638_test.go index 1db8a6f51..967195fd0 100644 --- a/openapi3/issue638_test.go +++ b/openapi3/issue638_test.go @@ -16,6 +16,6 @@ func TestIssue638(t *testing.T) { // testdata/issue638/test1.yaml : reproduce doc, err := loader.LoadFromFile("testdata/issue638/test1.yaml") require.NoError(t, err) - require.Equal(t, "int", doc.Components.Schemas["test1d"].Value.Type) + require.Equal(t, &Types{"int"}, doc.Components.Schemas["test1d"].Value.Type) } } diff --git a/openapi3/issue652_test.go b/openapi3/issue652_test.go index f36e92005..4036b4816 100644 --- a/openapi3/issue652_test.go +++ b/openapi3/issue652_test.go @@ -24,6 +24,6 @@ func TestIssue652(t *testing.T) { schema := spec.Components.Schemas[schemaName] assert.Equal(t, schema.Ref, "../definitions.yml#/components/schemas/TestSchema") - assert.Equal(t, schema.Value.Type, "string") + assert.Equal(t, schema.Value.Type, &openapi3.Types{"string"}) }) } diff --git a/openapi3/issue689_test.go b/openapi3/issue689_test.go index cafbadfac..44058a825 100644 --- a/openapi3/issue689_test.go +++ b/openapi3/issue689_test.go @@ -22,7 +22,7 @@ func TestIssue689(t *testing.T) { { name: "read-only property succeeds when read-only validation is disabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", ReadOnly: true}}), + "foo": {Type: &openapi3.Types{"boolean"}, ReadOnly: true}}), value: map[string]interface{}{"foo": true}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), @@ -32,7 +32,7 @@ func TestIssue689(t *testing.T) { { name: "non read-only property succeeds when read-only validation is disabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", ReadOnly: false}}), + "foo": {Type: &openapi3.Types{"boolean"}, ReadOnly: false}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest()}, value: map[string]interface{}{"foo": true}, @@ -41,7 +41,7 @@ func TestIssue689(t *testing.T) { { name: "read-only property fails when read-only validation is enabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", ReadOnly: true}}), + "foo": {Type: &openapi3.Types{"boolean"}, ReadOnly: true}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest()}, value: map[string]interface{}{"foo": true}, @@ -50,7 +50,7 @@ func TestIssue689(t *testing.T) { { name: "non read-only property succeeds when read-only validation is enabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", ReadOnly: false}}), + "foo": {Type: &openapi3.Types{"boolean"}, ReadOnly: false}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest()}, value: map[string]interface{}{"foo": true}, @@ -60,7 +60,7 @@ func TestIssue689(t *testing.T) { { name: "write-only property succeeds when write-only validation is disabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", WriteOnly: true}}), + "foo": {Type: &openapi3.Types{"boolean"}, WriteOnly: true}}), value: map[string]interface{}{"foo": true}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsResponse(), @@ -70,7 +70,7 @@ func TestIssue689(t *testing.T) { { name: "non write-only property succeeds when write-only validation is disabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", WriteOnly: false}}), + "foo": {Type: &openapi3.Types{"boolean"}, WriteOnly: false}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsResponse()}, value: map[string]interface{}{"foo": true}, @@ -79,7 +79,7 @@ func TestIssue689(t *testing.T) { { name: "write-only property fails when write-only validation is enabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", WriteOnly: true}}), + "foo": {Type: &openapi3.Types{"boolean"}, WriteOnly: true}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsResponse()}, value: map[string]interface{}{"foo": true}, @@ -88,7 +88,7 @@ func TestIssue689(t *testing.T) { { name: "non write-only property succeeds when write-only validation is enabled", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", WriteOnly: false}}), + "foo": {Type: &openapi3.Types{"boolean"}, WriteOnly: false}}), opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsResponse()}, value: map[string]interface{}{"foo": true}, diff --git a/openapi3/issue767_test.go b/openapi3/issue767_test.go index d498877c9..55fb52f44 100644 --- a/openapi3/issue767_test.go +++ b/openapi3/issue767_test.go @@ -21,7 +21,7 @@ func TestIssue767(t *testing.T) { { name: "default values disabled should fail with minProps 1", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", Default: true}}).WithMinProperties(1), + "foo": {Type: &openapi3.Types{"boolean"}, Default: true}}).WithMinProperties(1), value: map[string]interface{}{}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), @@ -31,7 +31,7 @@ func TestIssue767(t *testing.T) { { name: "default values enabled should pass with minProps 1", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", Default: true}}).WithMinProperties(1), + "foo": {Type: &openapi3.Types{"boolean"}, Default: true}}).WithMinProperties(1), value: map[string]interface{}{}, opts: []openapi3.SchemaValidationOption{ openapi3.VisitAsRequest(), @@ -42,8 +42,8 @@ func TestIssue767(t *testing.T) { { name: "default values enabled should pass with minProps 2", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", Default: true}, - "bar": {Type: "boolean"}, + "foo": {Type: &openapi3.Types{"boolean"}, Default: true}, + "bar": {Type: &openapi3.Types{"boolean"}}, }).WithMinProperties(2), value: map[string]interface{}{"bar": false}, opts: []openapi3.SchemaValidationOption{ @@ -55,8 +55,8 @@ func TestIssue767(t *testing.T) { { name: "default values enabled should fail with maxProps 1", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", Default: true}, - "bar": {Type: "boolean"}, + "foo": {Type: &openapi3.Types{"boolean"}, Default: true}, + "bar": {Type: &openapi3.Types{"boolean"}}, }).WithMaxProperties(1), value: map[string]interface{}{"bar": false}, opts: []openapi3.SchemaValidationOption{ @@ -68,8 +68,8 @@ func TestIssue767(t *testing.T) { { name: "default values disabled should pass with maxProps 1", schema: openapi3.NewSchema().WithProperties(map[string]*openapi3.Schema{ - "foo": {Type: "boolean", Default: true}, - "bar": {Type: "boolean"}, + "foo": {Type: &openapi3.Types{"boolean"}, Default: true}, + "bar": {Type: &openapi3.Types{"boolean"}}, }).WithMaxProperties(1), value: map[string]interface{}{"bar": false}, opts: []openapi3.SchemaValidationOption{ diff --git a/openapi3/load_cicular_ref_with_external_file_test.go b/openapi3/load_cicular_ref_with_external_file_test.go index 9bcaaf77f..7a99e7600 100644 --- a/openapi3/load_cicular_ref_with_external_file_test.go +++ b/openapi3/load_cicular_ref_with_external_file_test.go @@ -35,7 +35,7 @@ func TestLoadCircularRefFromFile(t *testing.T) { Value: &openapi3.Schema{ Properties: map[string]*openapi3.SchemaRef{ "id": { - Value: &openapi3.Schema{Type: "string"}}, + Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, }, }, }, diff --git a/openapi3/load_with_go_embed_test.go b/openapi3/load_with_go_embed_test.go index 3b77e5fe4..27b46b29f 100644 --- a/openapi3/load_with_go_embed_test.go +++ b/openapi3/load_with_go_embed_test.go @@ -40,5 +40,5 @@ func Example() { Properties["bar"].Value. Type, ) - // Output: string + // Output: &[string] } diff --git a/openapi3/loader_issue212_test.go b/openapi3/loader_issue212_test.go index 252d0d224..40721550a 100644 --- a/openapi3/loader_issue212_test.go +++ b/openapi3/loader_issue212_test.go @@ -79,11 +79,11 @@ components: require.NoError(t, err) expected, err := json.Marshal(&Schema{ - Type: "object", + Type: &Types{"object"}, Required: []string{"id", "uri"}, Properties: Schemas{ - "id": {Value: &Schema{Type: "string"}}, - "uri": {Value: &Schema{Type: "string"}}, + "id": {Value: &Schema{Type: &Types{"string"}}}, + "uri": {Value: &Schema{Type: &Types{"string"}}}, }, }, ) diff --git a/openapi3/loader_issue220_test.go b/openapi3/loader_issue220_test.go index 0b2569783..1b0efd64a 100644 --- a/openapi3/loader_issue220_test.go +++ b/openapi3/loader_issue220_test.go @@ -22,7 +22,7 @@ func TestIssue220(t *testing.T) { err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "integer", doc. + require.Equal(t, &Types{"integer"}, doc. Paths.Value("/foo"). Get.Responses.Value("200").Value. Content["application/json"]. diff --git a/openapi3/loader_outside_refs_test.go b/openapi3/loader_outside_refs_test.go index 3f2cf7cd7..7300c2c50 100644 --- a/openapi3/loader_outside_refs_test.go +++ b/openapi3/loader_outside_refs_test.go @@ -16,7 +16,7 @@ func TestLoadOutsideRefs(t *testing.T) { err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "string", doc. + require.Equal(t, &Types{"string"}, doc. Paths.Value("/service"). Get. Responses.Value("200").Value. diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go index 85655ef3e..adf91d2e1 100644 --- a/openapi3/loader_recursive_ref_test.go +++ b/openapi3/loader_recursive_ref_test.go @@ -52,7 +52,7 @@ components: require.NoError(t, err) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "object", doc.Components. + require.Equal(t, &Types{"object"}, doc.Components. Schemas["Complex"]. Value.Properties["parent"]. Value.Properties["parent"]. diff --git a/openapi3/loader_relative_refs_test.go b/openapi3/loader_relative_refs_test.go index 2ebd91b6d..cca44ba3a 100644 --- a/openapi3/loader_relative_refs_test.go +++ b/openapi3/loader_relative_refs_test.go @@ -27,7 +27,7 @@ var refTestDataEntries = []refTestDataEntry{ contentTemplate: externalSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Schemas["TestSchema"].Value.Type) - require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) + require.Equal(t, &Types{"string"}, doc.Components.Schemas["TestSchema"].Value.Type) }, }, { @@ -115,7 +115,7 @@ var refTestDataEntries = []refTestDataEntry{ contentTemplate: externalPathOperationParameterSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Schema.Value) - require.Equal(t, "string", doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Schema.Value.Type) + require.Equal(t, &Types{"string"}, doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Schema.Value.Type) require.Equal(t, "id", doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Name) }, }, @@ -126,7 +126,7 @@ var refTestDataEntries = []refTestDataEntry{ testFunc: func(t *testing.T, doc *T) { schemaRef := doc.Paths.Value("/test/{id}").Get.Parameters[0].Value.Content["application/json"].Schema require.NotNil(t, schemaRef.Value) - require.Equal(t, "string", schemaRef.Value.Type) + require.Equal(t, &Types{"string"}, schemaRef.Value.Type) }, }, @@ -143,7 +143,7 @@ var refTestDataEntries = []refTestDataEntry{ contentTemplate: externalPathOperationRequestBodyContentSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Schema.Value) - require.Equal(t, "string", doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, &Types{"string"}, doc.Paths.Value("/test").Post.RequestBody.Value.Content["application/json"].Schema.Value.Type) }, }, { @@ -163,7 +163,7 @@ var refTestDataEntries = []refTestDataEntry{ require.NotNil(t, doc.Paths.Value("/test").Post.Responses.Default().Value) desc := "testdescription" require.Equal(t, &desc, doc.Paths.Value("/test").Post.Responses.Default().Value.Description) - require.Equal(t, "string", doc.Paths.Value("/test").Post.Responses.Default().Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, &Types{"string"}, doc.Paths.Value("/test").Post.Responses.Default().Value.Content["application/json"].Schema.Value.Type) }, }, { @@ -171,7 +171,7 @@ var refTestDataEntries = []refTestDataEntry{ contentTemplate: externalComponentHeaderSchemaRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Headers["TestHeader"].Value) - require.Equal(t, "string", doc.Components.Headers["TestHeader"].Value.Schema.Value.Type) + require.Equal(t, &Types{"string"}, doc.Components.Headers["TestHeader"].Value.Schema.Value.Type) }, }, { @@ -725,7 +725,7 @@ var relativeDocRefsTestDataEntries = []refTestDataEntry{ contentTemplate: relativeSchemaDocsRefTemplate, testFunc: func(t *testing.T, doc *T) { require.NotNil(t, doc.Components.Schemas["TestSchema"].Value.Type) - require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) + require.Equal(t, &Types{"string"}, doc.Components.Schemas["TestSchema"].Value.Type) }, }, { @@ -927,7 +927,7 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { require.Equal(t, "example request", nestedDirPath.Patch.RequestBody.Value.Description) // check response schema and example - require.Equal(t, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type, "string") + require.Equal(t, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type, &Types{"string"}) expectedExample := "hello" require.Equal(t, expectedExample, nestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value) @@ -947,6 +947,6 @@ func TestLoadSpecWithRelativeDocumentRefs2(t *testing.T) { require.Equal(t, "example request", moreNestedDirPath.Patch.RequestBody.Value.Description) // check response schema and example - require.Equal(t, "string", moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, &Types{"string"}, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) require.Equal(t, moreNestedDirPath.Patch.Responses.Value("200").Value.Content["application/json"].Examples["CustomTestExample"].Value.Value, expectedExample) } diff --git a/openapi3/loader_test.go b/openapi3/loader_test.go index 18c28f156..7dcb3ed02 100644 --- a/openapi3/loader_test.go +++ b/openapi3/loader_test.go @@ -290,7 +290,7 @@ func TestLoadFromRemoteURL(t *testing.T) { doc, err := loader.LoadFromURI(url) require.NoError(t, err) - require.Equal(t, "string", doc.Components.Schemas["TestSchema"].Value.Type) + require.Equal(t, &Types{"string"}, doc.Components.Schemas["TestSchema"].Value.Type) } func TestLoadWithReferenceInReference(t *testing.T) { @@ -301,7 +301,7 @@ func TestLoadWithReferenceInReference(t *testing.T) { require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "string", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) + require.Equal(t, &Types{"string"}, doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) } func TestLoadWithRecursiveReferenceInLocalReferenceInParentSubdir(t *testing.T) { @@ -312,7 +312,7 @@ func TestLoadWithRecursiveReferenceInLocalReferenceInParentSubdir(t *testing.T) require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "object", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) + require.Equal(t, &Types{"object"}, doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["definition_reference"].Value.Type) } func TestLoadWithRecursiveReferenceInReferenceInLocalReference(t *testing.T) { @@ -323,7 +323,7 @@ func TestLoadWithRecursiveReferenceInReferenceInLocalReference(t *testing.T) { require.NotNil(t, doc) err = doc.Validate(loader.Context) require.NoError(t, err) - require.Equal(t, "integer", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Type) + require.Equal(t, &Types{"integer"}, doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Type) require.Equal(t, "int64", doc.Paths.Value("/api/test/ref/in/ref").Post.RequestBody.Value.Content["application/json"].Schema.Value.Properties["data"].Value.Properties["definition_reference"].Value.Properties["ref_prop_part"].Value.Properties["idPart"].Value.Format) } @@ -463,7 +463,7 @@ func TestLoadYamlFileWithExternalPathRef(t *testing.T) { require.NoError(t, err) require.NotNil(t, doc.Paths.Value("/test").Get.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) - require.Equal(t, "string", doc.Paths.Value("/test").Get.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) + require.Equal(t, &Types{"string"}, doc.Paths.Value("/test").Get.Responses.Value("200").Value.Content["application/json"].Schema.Value.Type) } func TestResolveResponseLinkRef(t *testing.T) { diff --git a/openapi3/refs_test.go b/openapi3/refs_test.go index e328c33eb..8a12c33c0 100644 --- a/openapi3/refs_test.go +++ b/openapi3/refs_test.go @@ -302,7 +302,7 @@ components: require.NotNil(t, v) require.IsType(t, &Schema{}, v) require.Equal(t, reflect.Ptr, kind) - require.Equal(t, "integer", v.(*Schema).Type) + require.Equal(t, &Types{"integer"}, v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/0") require.NoError(t, err) @@ -311,7 +311,7 @@ components: require.NotNil(t, v) require.IsType(t, &Schema{}, v) require.Equal(t, reflect.Ptr, kind) - require.Equal(t, "string", v.(*Schema).Type) + require.Equal(t, &Types{"string"}, v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/1") require.NoError(t, err) @@ -320,7 +320,7 @@ components: require.NotNil(t, v) require.IsType(t, &Schema{}, v) require.Equal(t, reflect.Ptr, kind) - require.Equal(t, "integer", v.(*Schema).Type) + require.Equal(t, &Types{"integer"}, v.(*Schema).Type) ptr, err = jsonpointer.New("/components/schemas/OneOfTest/oneOf/5") require.NoError(t, err) diff --git a/openapi3/schema.go b/openapi3/schema.go index e8630b3c2..32027a1eb 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -27,6 +27,7 @@ const ( TypeNumber = "number" TypeObject = "object" TypeString = "string" + TypeNull = "null" // constants for integer formats formatMinInt32 = float64(math.MinInt32) @@ -92,7 +93,7 @@ type Schema struct { AnyOf SchemaRefs `json:"anyOf,omitempty" yaml:"anyOf,omitempty"` AllOf SchemaRefs `json:"allOf,omitempty" yaml:"allOf,omitempty"` Not *SchemaRef `json:"not,omitempty" yaml:"not,omitempty"` - Type string `json:"type,omitempty" yaml:"type,omitempty"` + Type *Types `json:"type,omitempty" yaml:"type,omitempty"` Title string `json:"title,omitempty" yaml:"title,omitempty"` Format string `json:"format,omitempty" yaml:"format,omitempty"` Description string `json:"description,omitempty" yaml:"description,omitempty"` @@ -138,6 +139,75 @@ type Schema struct { Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"` } +type Types []string + +func (types *Types) Is(typ string) bool { + return types != nil && len(*types) == 1 && (*types)[0] == typ +} + +func (types *Types) Slice() []string { + if types == nil { + return nil + } + return *types +} + +func (pTypes *Types) Includes(typ string) bool { + if pTypes == nil { + return false + } + types := *pTypes + for _, candidate := range types { + if candidate == typ { + return true + } + } + return false +} + +func (types *Types) Permits(typ string) bool { + if types == nil { + return true + } + return types.Includes(typ) +} + +func (pTypes *Types) MarshalJSON() ([]byte, error) { + x, err := pTypes.MarshalYAML() + if err != nil { + return nil, err + } + return json.Marshal(x) +} + +func (pTypes *Types) MarshalYAML() (interface{}, error) { + if pTypes == nil { + return nil, nil + } + types := *pTypes + switch len(types) { + case 0: + return nil, nil + case 1: + return types[0], nil + default: + return []string(types), nil + } +} + +func (types *Types) UnmarshalJSON(data []byte) error { + var strings []string + if err := json.Unmarshal(data, &strings); err != nil { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return unmarshalError(err) + } + strings = []string{s} + } + *types = strings + return nil +} + type AdditionalProperties struct { Has *bool Schema *SchemaRef @@ -208,7 +278,7 @@ func (schema Schema) MarshalJSON() ([]byte, error) { if x := schema.Not; x != nil { m["not"] = x } - if x := schema.Type; len(x) != 0 { + if x := schema.Type; x != nil { m["type"] = x } if x := schema.Title; len(x) != 0 { @@ -530,72 +600,72 @@ func NewAllOfSchema(schemas ...*Schema) *Schema { func NewBoolSchema() *Schema { return &Schema{ - Type: TypeBoolean, + Type: &Types{TypeBoolean}, } } func NewFloat64Schema() *Schema { return &Schema{ - Type: TypeNumber, + Type: &Types{TypeNumber}, } } func NewIntegerSchema() *Schema { return &Schema{ - Type: TypeInteger, + Type: &Types{TypeInteger}, } } func NewInt32Schema() *Schema { return &Schema{ - Type: TypeInteger, + Type: &Types{TypeInteger}, Format: "int32", } } func NewInt64Schema() *Schema { return &Schema{ - Type: TypeInteger, + Type: &Types{TypeInteger}, Format: "int64", } } func NewStringSchema() *Schema { return &Schema{ - Type: TypeString, + Type: &Types{TypeString}, } } func NewDateTimeSchema() *Schema { return &Schema{ - Type: TypeString, + Type: &Types{TypeString}, Format: "date-time", } } func NewUUIDSchema() *Schema { return &Schema{ - Type: TypeString, + Type: &Types{TypeString}, Format: "uuid", } } func NewBytesSchema() *Schema { return &Schema{ - Type: TypeString, + Type: &Types{TypeString}, Format: "byte", } } func NewArraySchema() *Schema { return &Schema{ - Type: TypeArray, + Type: &Types{TypeArray}, } } func NewObjectSchema() *Schema { return &Schema{ - Type: TypeObject, + Type: &Types{TypeObject}, Properties: make(Schemas), } } @@ -770,9 +840,13 @@ func (schema *Schema) WithAdditionalProperties(v *Schema) *Schema { return schema } +func (schema *Schema) PermitsNull() bool { + return schema.Nullable || schema.Type.Includes("null") +} + // IsEmpty tells whether schema is equivalent to the empty schema `{}`. func (schema *Schema) IsEmpty() bool { - if schema.Type != "" || schema.Format != "" || len(schema.Enum) != 0 || + if schema.Type != nil || schema.Format != "" || len(schema.Enum) != 0 || schema.UniqueItems || schema.ExclusiveMin || schema.ExclusiveMax || schema.Nullable || schema.ReadOnly || schema.WriteOnly || schema.AllowEmptyValue || schema.Min != nil || schema.Max != nil || schema.MultipleOf != nil || @@ -887,64 +961,64 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema, } } - schemaType := schema.Type - switch schemaType { - case "": - case TypeBoolean: - case TypeNumber: - if format := schema.Format; len(format) > 0 { - switch format { - case "float", "double": - default: - if validationOpts.schemaFormatValidationEnabled { - return stack, unsupportedFormat(format) + for _, schemaType := range schema.Type.Slice() { + switch schemaType { + case TypeBoolean: + case TypeNumber: + if format := schema.Format; len(format) > 0 { + switch format { + case "float", "double": + default: + if validationOpts.schemaFormatValidationEnabled { + return stack, unsupportedFormat(format) + } } } - } - case TypeInteger: - if format := schema.Format; len(format) > 0 { - switch format { - case "int32", "int64": - default: - if validationOpts.schemaFormatValidationEnabled { - return stack, unsupportedFormat(format) + case TypeInteger: + if format := schema.Format; len(format) > 0 { + switch format { + case "int32", "int64": + default: + if validationOpts.schemaFormatValidationEnabled { + return stack, unsupportedFormat(format) + } } } - } - case TypeString: - if format := schema.Format; len(format) > 0 { - switch format { - // Supported by OpenAPIv3.0.3: - // https://spec.openapis.org/oas/v3.0.3 - case "byte", "binary", "date", "date-time", "password": - // In JSON Draft-07 (not validated yet though): - // https://json-schema.org/draft-07/json-schema-release-notes.html#formats - case "iri", "iri-reference", "uri-template", "idn-email", "idn-hostname": - case "json-pointer", "relative-json-pointer", "regex", "time": - // In JSON Draft 2019-09 (not validated yet though): - // https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary - case "duration", "uuid": - // Defined in some other specification - case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": - default: - // Try to check for custom defined formats - if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { - return stack, unsupportedFormat(format) + case TypeString: + if format := schema.Format; len(format) > 0 { + switch format { + // Supported by OpenAPIv3.0.3: + // https://spec.openapis.org/oas/v3.0.3 + case "byte", "binary", "date", "date-time", "password": + // In JSON Draft-07 (not validated yet though): + // https://json-schema.org/draft-07/json-schema-release-notes.html#formats + case "iri", "iri-reference", "uri-template", "idn-email", "idn-hostname": + case "json-pointer", "relative-json-pointer", "regex", "time": + // In JSON Draft 2019-09 (not validated yet though): + // https://json-schema.org/draft/2019-09/release-notes.html#format-vocabulary + case "duration", "uuid": + // Defined in some other specification + case "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference": + default: + // Try to check for custom defined formats + if _, ok := SchemaStringFormats[format]; !ok && validationOpts.schemaFormatValidationEnabled { + return stack, unsupportedFormat(format) + } } } - } - if !validationOpts.schemaPatternValidationDisabled && schema.Pattern != "" { - if _, err := schema.compilePattern(); err != nil { - return stack, err + if !validationOpts.schemaPatternValidationDisabled && schema.Pattern != "" { + if _, err := schema.compilePattern(); err != nil { + return stack, err + } } + case TypeArray: + if schema.Items == nil { + return stack, errors.New("when schema type is 'array', schema 'items' must be non-null") + } + case TypeObject: + default: + return stack, fmt.Errorf("unsupported 'type' value %q", schemaType) } - case TypeArray: - if schema.Items == nil { - return stack, errors.New("when schema type is 'array', schema 'items' must be non-null") - } - case TypeObject: - default: - return stack, fmt.Errorf("unsupported 'type' value %q", schemaType) } if ref := schema.Items; ref != nil { @@ -1053,7 +1127,7 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf case nil: // Don't use VisitJSONNull, as we still want to reach 'visitXOFOperations', since // those could allow for a nullable value even though this one doesn't - if schema.Nullable { + if schema.PermitsNull() { return } case float64: @@ -1371,7 +1445,7 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#data-types // https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object func (schema *Schema) visitJSONNull(settings *schemaValidationSettings) (err error) { - if schema.Nullable { + if schema.PermitsNull() { return } if settings.failfast { @@ -1392,7 +1466,7 @@ func (schema *Schema) VisitJSONBoolean(value bool) error { } func (schema *Schema) visitJSONBoolean(settings *schemaValidationSettings, value bool) (err error) { - if schemaType := schema.Type; schemaType != "" && schemaType != TypeBoolean { + if !schema.Type.Permits(TypeBoolean) { return schema.expectedType(settings, value) } return @@ -1406,7 +1480,9 @@ func (schema *Schema) VisitJSONNumber(value float64) error { func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value float64) error { var me MultiError schemaType := schema.Type - if schemaType == TypeInteger { + requireInteger := false + if schemaType.Permits(TypeInteger) && !schemaType.Permits(TypeNumber) { + requireInteger = true if bigFloat := big.NewFloat(value); !bigFloat.IsInt() { if settings.failfast { return errSchema @@ -1423,12 +1499,12 @@ func (schema *Schema) visitJSONNumber(settings *schemaValidationSettings, value } me = append(me, err) } - } else if schemaType != "" && schemaType != TypeNumber { + } else if !(schemaType.Permits(TypeInteger) || schemaType.Permits(TypeNumber)) { return schema.expectedType(settings, value) } // formats - if schemaType == TypeInteger && schema.Format != "" { + if requireInteger && schema.Format != "" { formatMin := float64(0) formatMax := float64(0) switch schema.Format { @@ -1568,7 +1644,7 @@ func (schema *Schema) VisitJSONString(value string) error { } func (schema *Schema) visitJSONString(settings *schemaValidationSettings, value string) error { - if schemaType := schema.Type; schemaType != "" && schemaType != TypeString { + if !schema.Type.Permits(TypeString) { return schema.expectedType(settings, value) } @@ -1703,7 +1779,7 @@ func (schema *Schema) VisitJSONArray(value []interface{}) error { } func (schema *Schema) visitJSONArray(settings *schemaValidationSettings, value []interface{}) error { - if schemaType := schema.Type; schemaType != "" && schemaType != TypeArray { + if !schema.Type.Permits(TypeArray) { return schema.expectedType(settings, value) } @@ -1802,7 +1878,7 @@ func (schema *Schema) VisitJSONObject(value map[string]interface{}) error { } func (schema *Schema) visitJSONObject(settings *schemaValidationSettings, value map[string]interface{}) error { - if schemaType := schema.Type; schemaType != "" && schemaType != TypeObject { + if !schema.Type.Permits(TypeObject) { return schema.expectedType(settings, value) } @@ -1986,15 +2062,23 @@ func (schema *Schema) expectedType(settings *schemaValidationSettings, value int } a := "a" - switch schema.Type { - case TypeArray, TypeObject, TypeInteger: - a = "an" + var x string + schemaTypes := (*schema.Type) + if len(schemaTypes) == 1 { + x = schemaTypes[0] + switch x { + case TypeArray, TypeObject, TypeInteger: + a = "an" + } + } else { + a = "one of" + x = strings.Join(schemaTypes, ", ") } return &SchemaError{ Value: value, Schema: schema, SchemaField: "type", - Reason: fmt.Sprintf("value must be %s %s", a, schema.Type), + Reason: fmt.Sprintf("value must be %s %s", a, x), customizeMessageError: settings.customizeMessageError, } } diff --git a/openapi3/schema_test.go b/openapi3/schema_test.go index fe3913907..b9f2dbf55 100644 --- a/openapi3/schema_test.go +++ b/openapi3/schema_test.go @@ -153,6 +153,40 @@ var schemaExamples = []schemaExample{ }, }, + { + Title: "PRIMITIVES WITHOUT NULL", + Schema: &Schema{ + Type: &Types{TypeString, TypeBoolean}, + }, + AllValid: []interface{}{ + "", + "xyz", + true, + false, + }, + AllInvalid: []interface{}{ + 1, + nil, + }, + }, + + { + Title: "PRIMITIVES WITH NULL", + Schema: &Schema{ + Type: &Types{TypeNumber, TypeNull}, + }, + AllValid: []interface{}{ + 0, + 1, + 2.3, + nil, + }, + AllInvalid: []interface{}{ + "x", + []interface{}{}, + }, + }, + { Title: "NULLABLE ANYOF", Schema: NewAnyOfSchema( @@ -509,7 +543,7 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY", Schema: &Schema{ - Type: "array", + Type: &Types{"array"}, MinItems: 2, MaxItems: Uint64Ptr(3), UniqueItems: true, @@ -549,10 +583,10 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY : items format 'object'", Schema: &Schema{ - Type: "array", + Type: &Types{"array"}, UniqueItems: true, Items: (&Schema{ - Type: "object", + Type: &Types{"object"}, Properties: Schemas{ "key1": NewFloat64Schema().NewRef(), }, @@ -606,13 +640,13 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY : items format 'object' and object with a property of array type ", Schema: &Schema{ - Type: "array", + Type: &Types{"array"}, UniqueItems: true, Items: (&Schema{ - Type: "object", + Type: &Types{"object"}, Properties: Schemas{ "key1": (&Schema{ - Type: "array", + Type: &Types{"array"}, UniqueItems: true, Items: NewFloat64Schema().NewRef(), }).NewRef(), @@ -692,10 +726,10 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY : items format 'array'", Schema: &Schema{ - Type: "array", + Type: &Types{"array"}, UniqueItems: true, Items: (&Schema{ - Type: "array", + Type: &Types{"array"}, UniqueItems: true, Items: NewFloat64Schema().NewRef(), }).NewRef(), @@ -736,13 +770,13 @@ var schemaExamples = []schemaExample{ { Title: "ARRAY : items format 'array' and array with object type items", Schema: &Schema{ - Type: "array", + Type: &Types{"array"}, UniqueItems: true, Items: (&Schema{ - Type: "array", + Type: &Types{"array"}, UniqueItems: true, Items: (&Schema{ - Type: "object", + Type: &Types{"object"}, Properties: Schemas{ "key1": NewFloat64Schema().NewRef(), }, @@ -840,7 +874,7 @@ var schemaExamples = []schemaExample{ { Title: "OBJECT", Schema: &Schema{ - Type: "object", + Type: &Types{"object"}, MaxProps: Uint64Ptr(2), Properties: Schemas{ "numberProperty": NewFloat64Schema().NewRef(), @@ -884,10 +918,10 @@ var schemaExamples = []schemaExample{ }, { Schema: &Schema{ - Type: "object", + Type: &Types{"object"}, AdditionalProperties: AdditionalProperties{Schema: &SchemaRef{ Value: &Schema{ - Type: "number", + Type: &Types{"number"}, }, }}, }, @@ -912,7 +946,7 @@ var schemaExamples = []schemaExample{ }, { Schema: &Schema{ - Type: "object", + Type: &Types{"object"}, AdditionalProperties: AdditionalProperties{Has: BoolPtr(true)}, }, Serialization: map[string]interface{}{ @@ -1399,7 +1433,7 @@ components: func TestValidationFailsOnInvalidPattern(t *testing.T) { schema := Schema{ Pattern: "[", - Type: "string", + Type: &Types{"string"}, } err := schema.Validate(context.Background()) @@ -1450,7 +1484,7 @@ enum: func TestIssue751(t *testing.T) { schema := &Schema{ - Type: "array", + Type: &Types{"array"}, UniqueItems: true, Items: NewStringSchema().NewRef(), } diff --git a/openapi3/unique_items_checker_test.go b/openapi3/unique_items_checker_test.go index e359aefb4..3dd72306a 100644 --- a/openapi3/unique_items_checker_test.go +++ b/openapi3/unique_items_checker_test.go @@ -11,7 +11,7 @@ import ( func TestRegisterArrayUniqueItemsChecker(t *testing.T) { var ( schema = openapi3.Schema{ - Type: "array", + Type: &openapi3.Types{"array"}, UniqueItems: true, Items: openapi3.NewStringSchema().NewRef(), } diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 8d9dca290..60afc54b3 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -194,7 +194,7 @@ func defaultContentParameterDecoder(param *openapi3.Parameter, values []string) unmarshal := func(encoded string, paramSchema *openapi3.SchemaRef) (decoded interface{}, err error) { if err = json.Unmarshal([]byte(encoded), &decoded); err != nil { - if paramSchema != nil && paramSchema.Value.Type != "object" { + if paramSchema != nil && !paramSchema.Value.Type.Is("object") { decoded, err = encoded, nil } } @@ -315,14 +315,14 @@ func decodeValue(dec valueDecoder, param string, sm *openapi3.SerializationMetho return nil, found, errors.New("not implemented: decoding 'not'") } - if schema.Value.Type != "" { + if schema.Value.Type != nil { var decodeFn func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) - switch schema.Value.Type { - case "array": + switch { + case schema.Value.Type.Is("array"): decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { return dec.DecodeArray(param, sm, schema) } - case "object": + case schema.Value.Type.Is("object"): decodeFn = func(param string, sm *openapi3.SerializationMethod, schema *openapi3.SchemaRef) (interface{}, bool, error) { return dec.DecodeObject(param, sm, schema) } @@ -505,7 +505,7 @@ func (d *urlValuesDecoder) DecodePrimitive(param string, sm *openapi3.Serializat return nil, ok, nil } - if schema.Value.Type == "" && schema.Value.Pattern != "" { + if schema.Value.Type == nil && schema.Value.Pattern != "" { return values[0], ok, nil } val, err := parsePrimitive(values[0], schema) @@ -693,7 +693,7 @@ func (d *urlValuesDecoder) DecodeObject(param string, sm *openapi3.Serialization break } - if schema.Value.Type == "array" || schema.Value.Type == "object" { + if schema.Value.Type.Permits("array") || schema.Value.Type.Permits("object") { for k := range props { path := strings.Split(k, urlDecoderDelimiter) if _, ok := deepGet(val, path...); ok { @@ -915,8 +915,8 @@ func findNestedSchema(parentSchema *openapi3.SchemaRef, keys []string) (*openapi func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string]interface{}, error) { obj := make(map[string]interface{}) for propName, propSchema := range schema.Value.Properties { - switch propSchema.Value.Type { - case "array": + switch { + case propSchema.Value.Type.Is("array"): vals := strings.Split(props[propName], urlDecoderDelimiter) for _, v := range vals { _, err := parsePrimitive(v, propSchema.Value.Items) @@ -925,7 +925,7 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string } } obj[propName] = vals - case "object": + case propSchema.Value.Type.Is("object"): for prop := range props { if !strings.HasPrefix(prop, propName+urlDecoderDelimiter) { continue @@ -935,7 +935,7 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string if err != nil { return nil, &ParseError{path: pathFromKeys(mapKeys), Reason: err.Error()} } - if nestedSchema.Value.Type == "array" { + if nestedSchema.Value.Type.Permits("array") { vals := strings.Split(props[prop], urlDecoderDelimiter) for _, v := range vals { _, err := parsePrimitive(v, nestedSchema.Value.Items) @@ -1005,40 +1005,50 @@ func parseArray(raw []string, schemaRef *openapi3.SchemaRef) ([]interface{}, err // parsePrimitive returns a value that is created by parsing a source string to a primitive type // that is specified by a schema. The function returns nil when the source string is empty. // The function panics when a schema has a non-primitive type. -func parsePrimitive(raw string, schema *openapi3.SchemaRef) (interface{}, error) { +func parsePrimitive(raw string, schema *openapi3.SchemaRef) (v interface{}, err error) { if raw == "" { return nil, nil } - switch schema.Value.Type { + for _, typ := range schema.Value.Type.Slice() { + v, err = parsePrimitiveCase(raw, schema, typ) + if err == nil { + return + } + } + return +} + +func parsePrimitiveCase(raw string, schema *openapi3.SchemaRef, typ string) (interface{}, error) { + switch typ { case "integer": if schema.Value.Format == "int32" { v, err := strconv.ParseInt(raw, 0, 32) if err != nil { - return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + typ, Cause: err.(*strconv.NumError).Err} } return int32(v), nil } v, err := strconv.ParseInt(raw, 0, 64) if err != nil { - return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + typ, Cause: err.(*strconv.NumError).Err} } return v, nil case "number": v, err := strconv.ParseFloat(raw, 64) if err != nil { - return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + typ, Cause: err.(*strconv.NumError).Err} } return v, nil case "boolean": v, err := strconv.ParseBool(raw) if err != nil { - return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + schema.Value.Type, Cause: err.(*strconv.NumError).Err} + return nil, &ParseError{Kind: KindInvalidFormat, Value: raw, Reason: "an invalid " + typ, Cause: err.(*strconv.NumError).Err} } return v, nil case "string": return raw, nil default: - return nil, &ParseError{Kind: KindOther, Value: raw, Reason: "schema has non primitive type " + schema.Value.Type} + return nil, &ParseError{Kind: KindOther, Value: raw, Reason: "schema has non primitive type " + typ} } } @@ -1165,16 +1175,17 @@ func urlencodedBodyDecoder(body io.Reader, header http.Header, schema *openapi3. // Validate schema of request body. // By the OpenAPI 3 specification request body's schema must have type "object". // Properties of the schema describes individual parts of request body. - if schema.Value.Type != "object" { + if !schema.Value.Type.Is("object") { return nil, errors.New("unsupported schema of request body") } for propName, propSchema := range schema.Value.Properties { - switch propSchema.Value.Type { - case "object": + propType := propSchema.Value.Type + switch { + case propType.Is("object"): return nil, fmt.Errorf("unsupported schema of request body's property %q", propName) - case "array": + case propType.Is("array"): items := propSchema.Value.Items.Value - if items.Type != "string" && items.Type != "integer" && items.Type != "number" && items.Type != "boolean" { + if !(items.Type.Is("string") || items.Type.Is("integer") || items.Type.Is("number") || items.Type.Is("boolean")) { return nil, fmt.Errorf("unsupported schema of request body's property %q", propName) } } @@ -1246,7 +1257,7 @@ func decodeProperty(dec valueDecoder, name string, prop *openapi3.SchemaRef, enc } func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) { - if schema.Value.Type != "object" { + if !schema.Value.Type.Is("object") { return nil, errors.New("unsupported schema of request body") } @@ -1309,7 +1320,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S return nil, &ParseError{Kind: KindOther, Cause: fmt.Errorf("part %s: undefined", name)} } } - if valueSchema.Value.Type == "array" { + if valueSchema.Value.Type.Is("array") { valueSchema = valueSchema.Value.Items } } @@ -1354,7 +1365,7 @@ func multipartBodyDecoder(body io.Reader, header http.Header, schema *openapi3.S if len(vv) == 0 { continue } - if prop.Value.Type == "array" { + if prop.Value.Type.Is("array") { obj[name] = vv } else { obj[name] = vv[0] diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go index 334af84b6..7e5f813d0 100644 --- a/openapi3filter/req_resp_decoder_test.go +++ b/openapi3filter/req_resp_decoder_test.go @@ -25,10 +25,10 @@ func TestDecodeParameter(t *testing.T) { explode = openapi3.BoolPtr(true) noExplode = openapi3.BoolPtr(false) arrayOf = func(items *openapi3.SchemaRef) *openapi3.SchemaRef { - return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "array", Items: items}} + return &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"array"}, Items: items}} } objectOf = func(args ...interface{}) *openapi3.SchemaRef { - s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "object", Properties: make(map[string]*openapi3.SchemaRef)}} + s := &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, Properties: make(map[string]*openapi3.SchemaRef)}} if len(args)%2 != 0 { panic("invalid arguments. must be an even number of arguments") } @@ -40,12 +40,12 @@ func TestDecodeParameter(t *testing.T) { return s } - integerSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "integer"}} - numberSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "number"}} - booleanSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "boolean"}} - stringSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "string"}} - additionalPropertiesObjectStringSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "object", AdditionalProperties: openapi3.AdditionalProperties{Schema: stringSchema}}} - additionalPropertiesObjectBoolSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: "object", AdditionalProperties: openapi3.AdditionalProperties{Schema: booleanSchema}}} + integerSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"integer"}}} + numberSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"number"}}} + booleanSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"boolean"}}} + stringSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}} + additionalPropertiesObjectStringSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, AdditionalProperties: openapi3.AdditionalProperties{Schema: stringSchema}}} + additionalPropertiesObjectBoolSchema = &openapi3.SchemaRef{Value: &openapi3.Schema{Type: &openapi3.Types{"object"}, AdditionalProperties: openapi3.AdditionalProperties{Schema: booleanSchema}}} allofSchema = &openapi3.SchemaRef{ Value: &openapi3.Schema{ AllOf: []*openapi3.SchemaRef{ diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go index eccabd85d..95805ee25 100644 --- a/openapi3gen/openapi3gen.go +++ b/openapi3gen/openapi3gen.go @@ -200,63 +200,63 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type return nil, nil // ignore case reflect.Bool: - schema.Type = "boolean" + schema.Type = &openapi3.Types{"boolean"} case reflect.Int: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} case reflect.Int8: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Min = &minInt8 schema.Max = &maxInt8 case reflect.Int16: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Min = &minInt16 schema.Max = &maxInt16 case reflect.Int32: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Format = "int32" case reflect.Int64: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Format = "int64" case reflect.Uint: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Min = &zeroInt case reflect.Uint8: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Min = &zeroInt schema.Max = &maxUint8 case reflect.Uint16: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Min = &zeroInt schema.Max = &maxUint16 case reflect.Uint32: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Min = &zeroInt schema.Max = &maxUint32 case reflect.Uint64: - schema.Type = "integer" + schema.Type = &openapi3.Types{"integer"} schema.Min = &zeroInt schema.Max = &maxUint64 case reflect.Float32: - schema.Type = "number" + schema.Type = &openapi3.Types{"number"} schema.Format = "float" case reflect.Float64: - schema.Type = "number" + schema.Type = &openapi3.Types{"number"} schema.Format = "double" case reflect.String: - schema.Type = "string" + schema.Type = &openapi3.Types{"string"} case reflect.Slice: if t.Elem().Kind() == reflect.Uint8 { if t == rawMessageType { return &openapi3.SchemaRef{Value: schema}, nil } - schema.Type = "string" + schema.Type = &openapi3.Types{"string"} schema.Format = "byte" } else { - schema.Type = "array" + schema.Type = &openapi3.Types{"array"} items, err := g.generateSchemaRefFor(parents, t.Elem(), name, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { @@ -272,7 +272,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type } case reflect.Map: - schema.Type = "object" + schema.Type = &openapi3.Types{"object"} additionalProperties, err := g.generateSchemaRefFor(parents, t.Elem(), name, tag) if err != nil { if _, ok := err.(*CycleError); ok && !g.opts.throwErrorOnCycle { @@ -288,7 +288,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type case reflect.Struct: if t == timeType { - schema.Type = "string" + schema.Type = &openapi3.Types{"string"} schema.Format = "date-time" } else { for _, fieldInfo := range typeInfo.Fields { @@ -344,7 +344,7 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type // Object only if it has properties if schema.Properties != nil { - schema.Type = "object" + schema.Type = &openapi3.Types{"object"} } } } @@ -366,13 +366,13 @@ func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Sche case reflect.Slice: ref := g.generateCycleSchemaRef(t.Elem(), schema) sliceSchema := openapi3.NewSchema() - sliceSchema.Type = "array" + sliceSchema.Type = &openapi3.Types{"array"} sliceSchema.Items = ref return openapi3.NewSchemaRef("", sliceSchema) case reflect.Map: ref := g.generateCycleSchemaRef(t.Elem(), schema) mapSchema := openapi3.NewSchema() - mapSchema.Type = "object" + mapSchema.Type = &openapi3.Types{"object"} mapSchema.AdditionalProperties = openapi3.AdditionalProperties{Schema: ref} return openapi3.NewSchemaRef("", mapSchema) default: diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go index 553affc0d..daea9ffd7 100644 --- a/openapi3gen/openapi3gen_test.go +++ b/openapi3gen/openapi3gen_test.go @@ -216,11 +216,11 @@ func TestExportedNonTagged(t *testing.T) { schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields()) require.NoError(t, err) require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ - Type: "object", + Type: &openapi3.Types{"object"}, Properties: map[string]*openapi3.SchemaRef{ - "A": {Value: &openapi3.Schema{Type: "string"}}, - "another": {Value: &openapi3.Schema{Type: "string"}}, - "even_a_yaml": {Value: &openapi3.Schema{Type: "string"}}, + "A": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "another": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, + "even_a_yaml": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, }}}, schemaRef) } @@ -383,11 +383,11 @@ func TestCyclicReferences(t *testing.T) { require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["FieldCycle"].Ref) require.NotNil(t, schemaRef.Value.Properties["SliceCycle"]) - require.Equal(t, "array", schemaRef.Value.Properties["SliceCycle"].Value.Type) + require.Equal(t, &openapi3.Types{"array"}, schemaRef.Value.Properties["SliceCycle"].Value.Type) require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["SliceCycle"].Value.Items.Ref) require.NotNil(t, schemaRef.Value.Properties["MapCycle"]) - require.Equal(t, "object", schemaRef.Value.Properties["MapCycle"].Value.Type) + require.Equal(t, &openapi3.Types{"object"}, schemaRef.Value.Properties["MapCycle"].Value.Type) require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Schema.Ref) } @@ -510,9 +510,9 @@ func TestSchemaCustomizerExcludeSchema(t *testing.T) { schema, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer) require.NoError(t, err) require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{ - Type: "object", + Type: &openapi3.Types{"object"}, Properties: map[string]*openapi3.SchemaRef{ - "Str": {Value: &openapi3.Schema{Type: "string"}}, + "Str": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}}, }}}, schema) customizer = openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error { diff --git a/routers/legacy/router_test.go b/routers/legacy/router_test.go index 99704b19f..2bc30ea83 100644 --- a/routers/legacy/router_test.go +++ b/routers/legacy/router_test.go @@ -195,7 +195,7 @@ func TestRouter(t *testing.T) { } schema := &openapi3.Schema{ - Type: "string", + Type: &openapi3.Types{"string"}, Example: 3, } content := openapi3.NewContentWithJSONSchema(schema)