diff --git a/components_test.go b/components_test.go index 36724bf..a5c08db 100644 --- a/components_test.go +++ b/components_test.go @@ -118,9 +118,6 @@ func TestComponents_Validate_Error(t *testing.T) { c openapi.Components err string }{ - {openapi.Components{ - Schemas: openapi.Schemas{"Pet": &openapi.Schema{}}, - }, `schemas["Pet"].type is required`}, {openapi.Components{ Schemas: openapi.Schemas{" ": &openapi.Schema{}}, }, `schemas[" "] (" ") is invalid: must match the regular expression "^[a-zA-Z0-9\\.\\-_]+$"`}, diff --git a/content_test.go b/content_test.go index 1c2c17a..d60d3fa 100644 --- a/content_test.go +++ b/content_test.go @@ -75,11 +75,6 @@ func TestContent_Validate_Error(t *testing.T) { {openapi.Content{ "not a real media type": &openapi.MediaType{}, }, `["not a real media type"]: mime: expected slash after first token`}, - {openapi.Content{ - openapi.MediaRangeJSON: &openapi.MediaType{ - Schema: &openapi.SchemaRef{Value: &openapi.Schema{}}, - }, - }, `["application/json"].schema.type is required`}, } { t.Run(tc.err, func(t *testing.T) { if err := tc.c.Validate(); err == nil || err.Error() != tc.err { diff --git a/document_test.go b/document_test.go index 18e1ec9..a9b927e 100644 --- a/document_test.go +++ b/document_test.go @@ -151,13 +151,6 @@ func TestDocumentValidate_Error(t *testing.T) { Schemas: openapi.Schemas{}, }, }, openapi.ErrEmptyDocument.Error()}, - {&openapi.Document{ - OpenAPI: "3.1.0", - Info: &openapi.Info{Title: "Sample API", Version: "1.0.0"}, - Components: openapi.Components{ - Schemas: openapi.Schemas{"Pet": &openapi.Schema{}}, - }, - }, `components.schemas["Pet"].type is required`}, {&openapi.Document{ OpenAPI: "3.1.0", Info: &openapi.Info{Title: "Sample API", Version: "1.0.0"}, diff --git a/header_test.go b/header_test.go index d7de961..30f4816 100644 --- a/header_test.go +++ b/header_test.go @@ -75,30 +75,12 @@ func TestHeader_Validate_Error(t *testing.T) { }, "schema and content are mutually exclusive", }, - { - openapi.Header{ - Schema: &openapi.Schema{}, - }, - "schema.type is required", - }, { openapi.Header{ Content: openapi.Content{}, }, "content is invalid: must contain exactly one entry, got 0", }, - { - openapi.Header{ - Content: openapi.Content{ - openapi.MediaRangeJSON: { - Schema: &openapi.SchemaRef{ - Value: &openapi.Schema{}, - }, - }, - }, - }, - `content["application/json"].schema.type is required`, - }, {openapi.Header{ Content: openapi.Content{openapi.MediaRangeJSON: {}}, Style: "foo", diff --git a/parameter_test.go b/parameter_test.go index 0c786c8..7ed2d6c 100644 --- a/parameter_test.go +++ b/parameter_test.go @@ -116,12 +116,6 @@ func TestParameter_Validate_Error(t *testing.T) { In: openapi.ParameterLocationPath, Required: true, }, "schema or content is required"}, - {openapi.Parameter{ - Name: "myname", - In: openapi.ParameterLocationPath, - Required: true, - Schema: &openapi.Schema{}, - }, "schema.type is required"}, {openapi.Parameter{ Name: "myname", In: openapi.ParameterLocationPath, diff --git a/response_test.go b/response_test.go index 44088a7..d4750c9 100644 --- a/response_test.go +++ b/response_test.go @@ -87,12 +87,6 @@ func TestResponse_Validate_Error(t *testing.T) { Description: "some description", Headers: openapi.Headers{"foo": {Value: &openapi.Header{}}}, }, `headers["foo"]: schema or content is required`}, - {openapi.Response{ - Description: "some description", - Content: openapi.Content{openapi.MediaRangeJSON: { - Schema: &openapi.SchemaRef{Value: &openapi.Schema{}}, - }}, - }, `content["application/json"].schema.type is required`}, {openapi.Response{ Description: "some description", Links: openapi.Links{"address": {Value: &openapi.Link{}}}, diff --git a/schema.go b/schema.go index e075653..882a36c 100644 --- a/schema.go +++ b/schema.go @@ -95,12 +95,12 @@ func setIndexSchema(s *Schema, idx int) *Schema { s.idx = idx; return s } func (s *Schema) Validate() error { s.Description = strings.TrimSpace(s.Description) - if s.Type == "" { - if len(s.AllOf) == 0 { - return &errpath.ErrField{Field: "type", Err: &errpath.ErrRequired{}} + // type is OPTIONAL — a schema without type accepts any value. + // See: https://spec.openapis.org/oas/v3.2.0.html#schema-object + if s.Type != "" { + if err := s.Type.Validate(); err != nil { + return &errpath.ErrField{Field: "type", Err: err} } - } else if err := s.Type.Validate(); err != nil { - return &errpath.ErrField{Field: "type", Err: err} } if s.Format != "" { @@ -113,14 +113,14 @@ func (s *Schema) Validate() error { switch s.Format { case "": // no format case FormatInt32, FormatInt64, FormatUint, FormatUint32, FormatUint64: - if s.Type != TypeInteger { + if s.Type != "" && s.Type != TypeInteger { return &errpath.ErrField{Field: "format", Err: &errpath.ErrInvalid[Format]{ Value: s.Format, Message: fmt.Sprintf("only valid for integer type, got %s", s.Type), }} } case FormatFloat, FormatDouble: - if s.Type != TypeNumber { + if s.Type != "" && s.Type != TypeNumber { return &errpath.ErrField{Field: "format", Err: &errpath.ErrInvalid[Format]{ Value: s.Format, Message: fmt.Sprintf("only valid for number type, got %s", s.Type), @@ -129,25 +129,25 @@ func (s *Schema) Validate() error { case FormatEmail, FormatPassword, FormatUUID, FormatURI, FormatURIRef, FormatZipCode, FormatIPv4, FormatIPv6: - if s.Type != TypeString { + if s.Type != "" && s.Type != TypeString { return &errpath.ErrField{Field: "format", Err: &errpath.ErrInvalid[Format]{ Value: s.Format, Message: fmt.Sprintf("only valid for string type, got %s", s.Type), }} } case FormatDuration, FormatDate, FormatDateTime: - switch s.Type { - case TypeInteger, TypeString: - default: - return &errpath.ErrField{Field: "format", Err: &errpath.ErrInvalid[Format]{ - Value: s.Format, - Message: fmt.Sprintf("only valid for integer or string type, got %s", s.Type), - }} + if s.Type != "" { + switch s.Type { + case TypeInteger, TypeString: + default: + return &errpath.ErrField{Field: "format", Err: &errpath.ErrInvalid[Format]{ + Value: s.Format, + Message: fmt.Sprintf("only valid for integer or string type, got %s", s.Type), + }} + } } case FormatByte, FormatBinary: - switch s.Type { - case TypeString: - default: + if s.Type != "" && s.Type != TypeString { return &errpath.ErrField{Field: "format", Err: &errpath.ErrInvalid[Format]{ Value: s.Format, Message: fmt.Sprintf("only valid for string type, got %s", s.Type), @@ -192,12 +192,12 @@ func (s *Schema) Validate() error { Message: fmt.Sprintf("minimum is greater than maximum (%v > %v)", *s.Min, *s.Max), }} } - } else if s.Min != nil { + } else if s.Type != "" && s.Min != nil { return &errpath.ErrField{Field: "minimum", Err: &errpath.ErrInvalid[float64]{ Value: *s.Min, Message: fmt.Sprintf("only valid for number type, got %s", s.Type), }} - } else if s.Max != nil { + } else if s.Type != "" && s.Max != nil { return &errpath.ErrField{Field: "maximum", Err: &errpath.ErrInvalid[float64]{ Value: *s.Max, Message: fmt.Sprintf("only valid for number type, got %s", s.Type), @@ -206,7 +206,7 @@ func (s *Schema) Validate() error { // String - if s.Type != TypeString && s.Enum != nil { + if s.Type != "" && s.Type != TypeString && s.Enum != nil { return &errpath.ErrField{Field: "enum", Err: &errpath.ErrInvalid[string]{ Message: fmt.Sprintf("only valid for string type, got %s", s.Type), }} @@ -233,17 +233,17 @@ func (s *Schema) Validate() error { return &errpath.ErrField{Field: "items", Err: err} } } - } else if s.MinItems != 0 { + } else if s.Type != "" && s.MinItems != 0 { return &errpath.ErrField{Field: "minItems", Err: &errpath.ErrInvalid[uint]{ Value: s.MinItems, Message: fmt.Sprintf("only valid for array type, got %s", s.Type), }} - } else if s.MaxItems != nil { + } else if s.Type != "" && s.MaxItems != nil { return &errpath.ErrField{Field: "maxItems", Err: &errpath.ErrInvalid[uint]{ Value: *s.MaxItems, Message: fmt.Sprintf("only valid for array type, got %s", s.Type), }} - } else if s.Items != nil { + } else if s.Type != "" && s.Items != nil { return &errpath.ErrField{Field: "items", Err: &errpath.ErrInvalid[string]{ Message: fmt.Sprintf("only valid for array type, got %s", s.Type), }} @@ -275,11 +275,11 @@ func (s *Schema) Validate() error { return &errpath.ErrField{Field: "additionalProperties", Err: err} } } - } else if s.Properties != nil { + } else if s.Type != "" && s.Properties != nil { return &errpath.ErrField{Field: "properties", Err: &errpath.ErrInvalid[string]{ Message: fmt.Sprintf("only valid for object type, got %s", s.Type), }} - } else if s.AdditionalProperties != nil { + } else if s.Type != "" && s.AdditionalProperties != nil { return &errpath.ErrField{Field: "additionalProperties", Err: &errpath.ErrInvalid[string]{ Message: fmt.Sprintf("only valid for object type, got %s", s.Type), }} @@ -289,7 +289,7 @@ func (s *Schema) Validate() error { switch dflt := s.Default.(type) { case nil: // empty case string: - if s.Type != TypeString { + if s.Type != "" && s.Type != TypeString { return &errpath.ErrField{Field: "default", Err: &errpath.ErrInvalid[string]{ Value: dflt, Message: fmt.Sprintf("does not match schema type, got %s", s.Type), diff --git a/schema_test.go b/schema_test.go index 4b9e765..bcb957b 100644 --- a/schema_test.go +++ b/schema_test.go @@ -26,6 +26,10 @@ func TestSchema_Validate(t *testing.T) { {Type: openapi.TypeInteger, Default: 3.0}, {Type: openapi.TypeInteger, Format: openapi.FormatDuration, Default: 3}, // e.g. seconds {Type: openapi.TypeString, Format: openapi.FormatByte}, // base64-encoded data + // type is OPTIONAL — a schema without type accepts any value + // See: https://spec.openapis.org/oas/v3.2.0.html#schema-object + {Description: "no type"}, + {}, } { t.Run(fmt.Sprintf("#%d", i), func(t *testing.T) { if err := tc.Validate(); err != nil { @@ -42,7 +46,6 @@ func TestSchema_Validate_Error(t *testing.T) { s openapi.Schema err string }{ - {openapi.Schema{}, "type is required"}, {openapi.Schema{ Type: "foo", }, `type ("foo") is invalid, must be one of: "integer", "number", "string", "array", "boolean", "object", "null"`}, @@ -127,32 +130,10 @@ func TestSchema_Validate_Error(t *testing.T) { MaxItems: pointer[uint](4), Items: &openapi.SchemaRef{}, }, `minItems (5) is invalid: minItems is greater than maxItems (5 > 4)`}, - {openapi.Schema{ - AllOf: openapi.SchemaRefList{ - {Value: &openapi.Schema{}}, - }, - }, `allOf[0].type is required`}, - {openapi.Schema{ - AllOf: openapi.SchemaRefList{ - {Value: &openapi.Schema{}}, - }, - }, `allOf[0].type is required`}, - {openapi.Schema{ - Type: openapi.TypeObject, - Properties: openapi.SchemaRefs{ - "foo": &openapi.SchemaRef{Value: &openapi.Schema{}}, - }, - }, `properties["foo"].type is required`}, {openapi.Schema{ Type: openapi.TypeObject, Required: []string{"foo"}, }, `required[0] ("foo") is invalid: property does not exist`}, - {openapi.Schema{ - Type: openapi.TypeObject, - AdditionalProperties: &openapi.SchemaRef{ - Value: &openapi.Schema{}, - }, - }, `additionalProperties.type is required`}, {openapi.Schema{ Type: openapi.TypeBoolean, Properties: openapi.SchemaRefs{}, diff --git a/vendor/github.com/MarkRosemaker/jsonutil/http_header.go b/vendor/github.com/MarkRosemaker/jsonutil/http_header.go index 8f499ed..8dc270e 100644 --- a/vendor/github.com/MarkRosemaker/jsonutil/http_header.go +++ b/vendor/github.com/MarkRosemaker/jsonutil/http_header.go @@ -47,22 +47,22 @@ func HTTPHeaderUnmarshal(dec *jsontext.Decoder, h *http.Header) error { } switch tkn.Kind() { - case jsontext.KindBeginObject: // expected, continue below + case '{': // expected, continue below *h = http.Header{} - case jsontext.KindNull: + case 'n': *h = nil return nil // nil map default: return fmt.Errorf("expected begin object, got %s", tkn.Kind()) } - for dec.PeekKind() != jsontext.KindEndObject { + for dec.PeekKind() != '}' { keyTkn, err := dec.ReadToken() if err != nil { return err } - if keyTkn.Kind() != jsontext.KindString { + if keyTkn.Kind() != '"' { return fmt.Errorf("expected string key, got %s", keyTkn.Kind()) } @@ -73,7 +73,7 @@ func HTTPHeaderUnmarshal(dec *jsontext.Decoder, h *http.Header) error { return err } - if val.Kind() != jsontext.KindString { + if val.Kind() != '"' { return fmt.Errorf("expected string value, got %s", val.Kind()) } diff --git a/vendor/github.com/MarkRosemaker/jsonutil/url.go b/vendor/github.com/MarkRosemaker/jsonutil/url.go index 34bd7b2..6b81765 100644 --- a/vendor/github.com/MarkRosemaker/jsonutil/url.go +++ b/vendor/github.com/MarkRosemaker/jsonutil/url.go @@ -19,7 +19,7 @@ func URLUnmarshal(dec *jsontext.Decoder, u *url.URL) error { } switch tkn.Kind() { - case jsontext.KindString: + case '"': parsed, err := url.Parse(tkn.String()) if err != nil { return err @@ -28,7 +28,7 @@ func URLUnmarshal(dec *jsontext.Decoder, u *url.URL) error { *u = *parsed return nil - case jsontext.KindNull: + case 'n': return nil // no url given default: return fmt.Errorf("expected string, got %s", tkn)