Repository: getkin/kin-openapi
Pull request #738 (2023-01-10 05:22:51)

Specify UseNumber() in the JSON decoder during JSON validation
Partial fix for https://github.com/getkin/kin-openapi/issues/733.


fenollp: Hey, thanks for your work!
Since this is only a partial fix, I'm wondering what the plan forward should be. 
From https://github.com/golang/go/issues/6384#issue-51285102:
> (There are other issues as you get to very large values, since float64 can't encode a
whole int64... not sure what or if anything can be done about that.)

Indeed!
Should we be turning all numbers `Schema` holds to `big.Int` or `Float` or `Rat`?


Patch:
diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml
index b38f10f..8dbb062 100644
--- a/.github/workflows/go.yml
+++ b/.github/workflows/go.yml
@@ -79,6 +79,9 @@ jobs:
     - run: go test -v -run TestRaceyPatternSchema -race ./...
       env:
         CGO_ENABLED: '1'
+    - run: go test -v -run TestIssue741 -race ./...
+      env:
+        CGO_ENABLED: '1'
     - run: git --no-pager diff --exit-code
 
     - if: runner.os == 'Linux'
diff --git a/openapi3/loader_uri_reader.go b/openapi3/loader_uri_reader.go
index 8357a98..92ac043 100644
--- a/openapi3/loader_uri_reader.go
+++ b/openapi3/loader_uri_reader.go
@@ -7,12 +7,15 @@ import (
 	"net/http"
 	"net/url"
 	"path/filepath"
+	"sync"
 )
 
 // ReadFromURIFunc defines a function which reads the contents of a resource
 // located at a URI.
 type ReadFromURIFunc func(loader *Loader, url *url.URL) ([]byte, error)
 
+var uriMu = &sync.RWMutex{}
+
 // ErrURINotSupported indicates the ReadFromURIFunc does not know how to handle a
 // given URI.
 var ErrURINotSupported = errors.New("unsupported URI")
@@ -92,12 +95,17 @@ func URIMapCache(reader ReadFromURIFunc) ReadFromURIFunc {
 		}
 		uri := location.String()
 		var ok bool
+		uriMu.RLock()
 		if buf, ok = cache[uri]; ok {
+			uriMu.RUnlock()
 			return
 		}
+		uriMu.RUnlock()
 		if buf, err = reader(loader, location); err != nil {
 			return
 		}
+		uriMu.Lock()
+		defer uriMu.Unlock()
 		cache[uri] = buf
 		return
 	}
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 9d21c00..25d244a 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -1077,6 +1077,19 @@ func (schema *Schema) visitJSON(settings *schemaValidationSettings, value interf
 	switch value := value.(type) {
 	case bool:
 		return schema.visitJSONBoolean(settings, value)
+	case json.Number:
+		valueFloat64, err := value.Float64()
+		if err != nil {
+			return &SchemaError{
+				Value:                 value,
+				Schema:                schema,
+				SchemaField:           "type",
+				Reason:                "cannot convert json.Number to float64",
+				customizeMessageError: settings.customizeMessageError,
+				Origin:                err,
+			}
+		}
+		return schema.visitJSONNumber(settings, valueFloat64)
 	case int:
 		return schema.visitJSONNumber(settings, float64(value))
 	case int32:
diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go
index 44abacb..384b512 100644
--- a/openapi3filter/req_resp_decoder.go
+++ b/openapi3filter/req_resp_decoder.go
@@ -1030,7 +1030,9 @@ func plainBodyDecoder(body io.Reader, header http.Header, schema *openapi3.Schem
 
 func jsonBodyDecoder(body io.Reader, header http.Header, schema *openapi3.SchemaRef, encFn EncodingFn) (interface{}, error) {
 	var value interface{}
-	if err := json.NewDecoder(body).Decode(&value); err != nil {
+	dec := json.NewDecoder(body)
+	dec.UseNumber()
+	if err := dec.Decode(&value); err != nil {
 		return nil, &ParseError{Kind: KindInvalidFormat, Cause: err}
 	}
 	return value, nil


Test patch:
diff --git a/openapi3/issue741_test.go b/openapi3/issue741_test.go
new file mode 100644
index 0000000..aad5220
--- /dev/null
+++ b/openapi3/issue741_test.go
@@ -0,0 +1,43 @@
+package openapi3
+
+import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"sync"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIssue741(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		body := `{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Foo":{"type":"string"}}}}`
+		_, err := w.Write([]byte(body))
+		if err != nil {
+			panic(err)
+		}
+	}))
+	defer ts.Close()
+
+	rootSpec := []byte(fmt.Sprintf(
+		`{"openapi":"3.0.0","info":{"title":"MyAPI","version":"0.1","description":"An API"},"paths":{},"components":{"schemas":{"Bar1":{"$ref":"%s#/components/schemas/Foo"}}}}`,
+		ts.URL,
+	))
+
+	wg := &sync.WaitGroup{}
+	n := 10
+	for i := 0; i < n; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+			loader := NewLoader()
+			loader.IsExternalRefsAllowed = true
+			doc, err := loader.LoadFromData(rootSpec)
+			require.NoError(t, err)
+			require.NotNil(t, doc)
+		}()
+	}
+	wg.Wait()
+}
diff --git a/openapi3filter/issue733_test.go b/openapi3filter/issue733_test.go
new file mode 100644
index 0000000..0d2214b
--- /dev/null
+++ b/openapi3filter/issue733_test.go
@@ -0,0 +1,109 @@
+package openapi3filter_test
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"math"
+	"math/big"
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/openapi3filter"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func TestIntMax(t *testing.T) {
+	spec := `
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: test large integer value
+paths:
+  /test:
+    post:
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                testInteger:
+                  type: integer
+                  format: int64
+                testDefault:
+                  type: boolean
+                  default: false
+      responses:
+        '200':
+          description: Successful response
+`[1:]
+
+	loader := openapi3.NewLoader()
+
+	doc, err := loader.LoadFromData([]byte(spec))
+	require.NoError(t, err)
+
+	err = doc.Validate(loader.Context)
+	require.NoError(t, err)
+
+	router, err := gorillamux.NewRouter(doc)
+	require.NoError(t, err)
+
+	testOne := func(value *big.Int, pass bool) {
+		valueString := value.String()
+
+		req, err := http.NewRequest(http.MethodPost, "/test", bytes.NewReader([]byte(`{"testInteger":`+valueString+`}`)))
+		require.NoError(t, err)
+		req.Header.Set("Content-Type", "application/json")
+
+		route, pathParams, err := router.FindRoute(req)
+		require.NoError(t, err)
+
+		err = openapi3filter.ValidateRequest(
+			context.Background(),
+			&openapi3filter.RequestValidationInput{
+				Request:    req,
+				PathParams: pathParams,
+				Route:      route,
+			})
+		if pass {
+			require.NoError(t, err)
+
+			dec := json.NewDecoder(req.Body)
+			dec.UseNumber()
+			var jsonAfter map[string]interface{}
+			err = dec.Decode(&jsonAfter)
+			require.NoError(t, err)
+
+			valueAfter := jsonAfter["testInteger"]
+			require.IsType(t, json.Number(""), valueAfter)
+			assert.Equal(t, valueString, string(valueAfter.(json.Number)))
+		} else {
+			if assert.Error(t, err) {
+				var serr *openapi3.SchemaError
+				if assert.ErrorAs(t, err, &serr) {
+					assert.Equal(t, "number must be an int64", serr.Reason)
+				}
+			}
+		}
+	}
+
+	bigMaxInt64 := big.NewInt(math.MaxInt64)
+	bigMaxInt64Plus1 := new(big.Int).Add(bigMaxInt64, big.NewInt(1))
+	bigMinInt64 := big.NewInt(math.MinInt64)
+	bigMinInt64Minus1 := new(big.Int).Sub(bigMinInt64, big.NewInt(1))
+
+	testOne(bigMaxInt64, true)
+	// XXX not yet fixed
+	// testOne(bigMaxInt64Plus1, false)
+	testOne(bigMaxInt64Plus1, true)
+	testOne(bigMinInt64, true)
+	// XXX not yet fixed
+	// testOne(bigMinInt64Minus1, false)
+	testOne(bigMinInt64Minus1, true)
+}
diff --git a/openapi3filter/req_resp_decoder_test.go b/openapi3filter/req_resp_decoder_test.go
index 1e71e0a..449ba0e 100644
--- a/openapi3filter/req_resp_decoder_test.go
+++ b/openapi3filter/req_resp_decoder_test.go
@@ -1226,7 +1226,7 @@ func TestDecodeBody(t *testing.T) {
 				WithProperty("d", openapi3.NewObjectSchema().WithProperty("d1", openapi3.NewStringSchema())).
 				WithProperty("f", openapi3.NewStringSchema().WithFormat("binary")).
 				WithProperty("g", openapi3.NewStringSchema()),
-			want: map[string]interface{}{"a": "a1", "b": float64(10), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"},
+			want: map[string]interface{}{"a": "a1", "b": json.Number("10"), "c": []interface{}{"c1", "c2"}, "d": map[string]interface{}{"d1": "d1"}, "f": "foo", "g": "g1"},
 		},
 		{
 			name: "multipartExtraPart",