kin-openapi icon indicating copy to clipboard operation
kin-openapi copied to clipboard

integer support broken with yaml

Open cboitel opened this issue 4 years ago • 4 comments

Two use cases:

  1. use openapi3 to load a document and ensure some data unarmshaled from a file complies with one of the schema (using VisitJSON)
  2. 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.

cboitel avatar Jan 31 '22 10:01 cboitel

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.

fenollp avatar Feb 23 '22 11:02 fenollp

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

cboitel avatar Feb 23 '22 20:02 cboitel

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.

cboitel avatar Feb 23 '22 20:02 cboitel

Submitted a PR to:

  1. fix this (also added tests with YAML decoded data not only JSON data)
  2. enhance integer support (added limit checks for int32/int64 formats)

cboitel avatar Aug 02 '22 06:08 cboitel