integer support broken with yaml
Two use cases:
- use openapi3 to load a document and ensure some data unarmshaled from a file complies with one of the schema (using VisitJSON)
- use openapi3.filter to validate a request complies with a loaded openapi document
In both use cases, if a schema references a type integer and data was unmarshaled from YAML, we end up with " unhandled value of type int" error. If file or request is using JSON, no error is reported. => for a file, one can workaround the issue by marshaling the read YAML to JSON and unmarshal it back from JSON => for openapi3filter, no workaround found as of today.
I believe this is due to the wrong assumption that all numbers will be using float type as this is the case for JSON (no integers, only floats => json unmarshaling returns float only) => https://github.com/getkin/kin-openapi/blob/master/openapi3/schema.go#L795 only references float => https://github.com/getkin/kin-openapi/blob/master/openapi3/schema.go#L998 checks for integers but not its format (int32/64)
Support for integers type and its associated formats (int32/int64) is required for non-JSON content-types.
Sounds to me you're experiencing unintuitive YAML integer parsing but it might be something else. Please provide minimal code reproducing the above mentioned error.
Here is an example:
package main
import (
"bytes"
"context"
"fmt"
"net/http"
"github.com/getkin/kin-openapi/openapi3"
"github.com/getkin/kin-openapi/openapi3filter"
legacyrouter "github.com/getkin/kin-openapi/routers/legacy"
)
var openapiDoc = []byte(`
openapi: 3.0.3
info:
version: v0.1.0
title: Integer Sample API
paths:
/samples:
post:
description: Sample operation
requestBody:
content:
application/yaml:
schema:
$ref: '#/components/schemas/sample'
application/json:
schema:
$ref: '#/components/schemas/sample'
responses:
"200":
description: ok
components:
schemas:
sample:
type: object
properties:
integer:
type: integer
number:
type: number
`)
func main() {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData(openapiDoc)
if err != nil {
panic(err)
}
err = doc.Validate(loader.Context)
if err != nil {
panic(err)
}
router, err := legacyrouter.NewRouter(doc)
if err != nil {
panic(err)
}
testBodyJson := bytes.NewBuffer([]byte(`{ "integer": 1, "number": 2 }`))
httpReqJson, _ := http.NewRequest(http.MethodPost, "/samples", testBodyJson)
httpReqJson.Header.Set("Content-Type", "application/json")
// Find route
routeJson, pathParamsJson, _ := router.FindRoute(httpReqJson)
// Validate request
jsonRequestValidationInput := &openapi3filter.RequestValidationInput{
Request: httpReqJson,
PathParams: pathParamsJson,
Route: routeJson,
}
if err := openapi3filter.ValidateRequest(context.Background(), jsonRequestValidationInput); err != nil {
panic(err)
}
fmt.Println("JSON valid request")
testBodyYaml := bytes.NewBuffer([]byte(`
integer: 1
number: 2.0`))
httpReqYaml, _ := http.NewRequest(http.MethodPost, "/samples", testBodyYaml)
httpReqYaml.Header.Set("Content-Type", "application/yaml")
// Find route
routeYaml, pathParamsYaml, _ := router.FindRoute(httpReqYaml)
// Validate request
yamlRequestValidationInput := &openapi3filter.RequestValidationInput{
Request: httpReqYaml,
PathParams: pathParamsYaml,
Route: routeYaml,
}
if err := openapi3filter.ValidateRequest(context.Background(), yamlRequestValidationInput); err != nil {
panic(err)
}
fmt.Println("Yaml valid request")
}
Output gives:
JSON valid request
panic: request body has an error: doesn't match the schema: Error at "/integer": unhandled value of type int
Schema:
{
"type": "integer"
}
Value:
1
goroutine 1 [running]:
main.main()
/home/cbtl/openapi3-yaml/main.go:93 +0x594
exit status 2
If you change yaml body to:
testBodyYaml := bytes.NewBuffer([]byte(`
integer: 1.0
number: 2.0`))
it succeeds while it should not (integer is no longer an integer):
JSON valid request
Yaml valid request
Note that the following code demonstrate that json encoding always return float64 even for integers while yaml returns int
package main
import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
)
func main() {
var jsonData map[string]interface{}
err := json.Unmarshal([]byte(`{ "integer": 1, "number": 2 }`), &jsonData)
if err != nil {
panic(err)
}
fmt.Printf("jsonData.integer = %T(%v)\n", jsonData["integer"], jsonData["integer"])
fmt.Printf("jsonData.number = %T(%v)\n", jsonData["number"], jsonData["number"])
var yamlData map[string]interface{}
err = yaml.Unmarshal([]byte(`
integer: 1
number: 2
`), &yamlData)
if err != nil {
panic(err)
}
fmt.Printf("yamlData.integer = %T(%v)\n", yamlData["integer"], yamlData["integer"])
fmt.Printf("yamlData.number = %T(%v)\n", yamlData["number"], yamlData["number"])
}
Output:
jsonData.integer = float64(1)
jsonData.number = float64(2)
yamlData.integer = int(1)
yamlData.number = int(2)
Since kinopenapi only handles float64 as returned by json decoding, int from yaml aren't supported. Note that encoding/json returns floats since in Javascript all numbers are floats.
Submitted a PR to:
- fix this (also added tests with YAML decoded data not only JSON data)
- enhance integer support (added limit checks for int32/int64 formats)