From c0005bea2676a3750025bd0e08efcecd956b8d83 Mon Sep 17 00:00:00 2001
From: Pierre Humberdroz
Date: Sun, 1 Mar 2026 18:17:22 +0000
Subject: [PATCH 1/2] Update validation error handling
---
.golangci.yaml | 408 +++++--------------
README.md | 61 ++-
pkg/gin-openapi-validator/validationerror.go | 1 +
3 files changed, 159 insertions(+), 311 deletions(-)
diff --git a/.golangci.yaml b/.golangci.yaml
index cf230d8..dff1423 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -1,245 +1,18 @@
-# This file contains all available configuration options
-# with their default values.
+version: "2"
-# options for analysis running
run:
- # default concurrency is a available CPU number
- # concurrency: 4
-
- # timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 30m
-
- # exit code when at least one issue was found, default is 1
- issues-exit-code: 1
-
- # include test files or not, default is true
tests: true
- # list of build tags, all linters use it. Default is empty list.
- # build-tags:
- # - mytag
-
- # which dirs to skip: issues from them won't be reported;
- # can use regexp here: generated.*, regexp is applied on full path;
- # default value is empty list, but default dirs are skipped independently
- # from this option's value (see skip-dirs-use-default).
- # skip-dirs:
- # - src/external_libs
- # - autogenerated_by_my_lib
-
- # default is true. Enables skipping of directories:
- # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
- skip-dirs-use-default: true
-
- # which files to skip: they will be analyzed, but issues from them
- # won't be reported. Default value is empty list, but there is
- # no need to include all autogenerated files, we confidently recognize
- # autogenerated files. If it's not please let us know.
- # skip-files:
- # - ".*\\.my\\.go$"
- # - lib/bad.go
-
- # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
- # If invoked with -mod=readonly, the go command is disallowed from the implicit
- # automatic updating of go.mod described above. Instead, it fails when any changes
- # to go.mod are needed. This setting is most useful to check that go.mod does
- # not need updates, such as in a continuous integration and testing system.
- # If invoked with -mod=vendor, the go command assumes that the vendor
- # directory holds the correct copies of dependencies and ignores
- # the dependency descriptions in go.mod.
- # modules-download-mode: readonly|release|vendor
-
-# output configuration options
output:
- # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
- format: line-number
-
- # print lines of code with issue, default is true
- print-issued-lines: true
-
- # print linter name in the end of issue text, default is true
- print-linter-name: true
-
-# all available settings of specific linters
-linters-settings:
- errcheck:
- # report about not checking of errors in type assetions: `a := b.(MyStruct)`;
- # default is false: such cases aren't reported by default.
- check-type-assertions: false
-
- # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
- # default is false: such cases aren't reported by default.
- check-blank: false
-
- # [deprecated] comma-separated list of pairs of the form pkg:regex
- # the regex is used to ignore names within pkg. (default "fmt:.*").
- # see https://github.com/kisielk/errcheck#the-deprecated-method for details
- # ignore: fmt:.*,io/ioutil:^Read.*
-
- # path to a file containing a list of functions to exclude from checking
- # see https://github.com/kisielk/errcheck#excluding-functions for details
- # exclude: /path/to/file.txt
-
- # Disable error checking, as errorcheck detects more errors and is more configurable.
- gosec:
- exclude:
- - "G104"
-
- funlen:
- lines: 60
- statements: 40
-
- govet:
- # report about shadowed variables
- check-shadowing: true
-
- # settings per analyzer
- settings:
- printf: # analyzer name, run `go tool vet help` to see all analyzers
- funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
- - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
-
- # enable or disable analyzers by name
- # enable:
- # - atomicalign
- # enable-all: false
- # disable:
- # - shadow
- # disable-all: false
- golint:
- # minimal confidence for issues, default is 0.8
- min-confidence: 0.8
- gofmt:
- # simplify code: gofmt with `-s` option, true by default
- simplify: true
- goimports:
- # put imports beginning with prefix after 3rd-party packages;
- # it's a comma-separated list of prefixes
- # local-prefixes: github.com/org/project
- gocyclo:
- # minimal code complexity to report, 30 by default (but we recommend 10-20)
- min-complexity: 30
- gocognit:
- # minimal code complexity to report, 30 by default (but we recommend 10-20)
- min-complexity: 20
- maligned:
- # print struct with more effective memory layout or not, false by default
- suggest-new: true
- dupl:
- # tokens count to trigger issue, 150 by default
- threshold: 100
- goconst:
- # minimal length of string constant, 3 by default
- min-len: 3
- # minimal occurrences count to trigger, 3 by default
- min-occurrences: 3
- # depguard:
- # list-type: blacklist
- # include-go-root: false
- # packages:
- # - github.com/sirupsen/logrus
- # packages-with-error-messages:
- # # specify an error message to output when a blacklisted package is used
- # github.com/sirupsen/logrus: "logging is allowed only by logutils.Log"
- misspell:
- # Correct spellings using locale preferences for US or UK.
- # Default is to use a neutral variety of English.
- # Setting locale to US will correct the British spelling of 'colour' to 'color'.
- locale: US
- ignore-words:
- - GitLab
- lll:
- # max line length, lines longer will be reported. Default is 120.
- # '\t' is counted as 1 character by default, and can be changed with the tab-width option
- line-length: 120
- # tab width in spaces. Default to 1.
- tab-width: 1
- unused:
- # treat code as a program (not a library) and report unused exported identifiers; default is false.
- # XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
- # if it's called for subdir of a project it can't find funcs usages. All text editor integrations
- # with golangci-lint call it on a directory with the changed file.
- check-exported: false
- unparam:
- # Inspect exported functions, default is false. Set to true if no external program/library imports your code.
- # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
- # if it's called for subdir of a project it can't find external interfaces. All text editor integrations
- # with golangci-lint call it on a directory with the changed file.
- check-exported: false
- nakedret:
- # make an issue if func has more lines of code than this setting and it has naked returns; default is 30
- max-func-lines: 30
- prealloc:
- # XXX: we don't recommend using this linter before doing performance profiling.
- # For most programs usage of prealloc will be a premature optimization.
-
- # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
- # True by default.
- simple: true
- range-loops: true # Report preallocation suggestions on range loops, true by default
- for-loops: false # Report preallocation suggestions on for loops, false by default
- gocritic:
- # Which checks should be enabled; can't be combined with 'disabled-checks';
- # See https://go-critic.github.io/overview#checks-overview
- # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`
- # By default list of stable checks is used.
- # enabled-checks:
- # - rangeValCopy
-
- # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty
- # disabled-checks:
- # - regexpMust
-
- # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
- # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags".
- # enabled-tags:
- # - performance
-
- settings: # settings passed to gocritic
- captLocal: # must be valid enabled check name
- paramsOnly: true
- # rangeValCopy:
- # sizeThreshold: 32
- godox:
- # report any comments starting with keywords, this is useful for TODO or FIXME comments that
- # might be left in the code accidentally and should be resolved before merging
- keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting
- - TODO
- - BUG
- - FIXME
- - NOTE
- - OPTIMIZE # marks code that should be optimized before merging
- - HACK # marks hack-arounds that should be removed before merging
- dogsled:
- # checks assignments with too many blank identifiers; default is 2
- max-blank-identifiers: 2
-
- whitespace:
- multi-if: false # Enforces newlines (or comments) after every multi-line if statement
- multi-func: false # Enforces newlines (or comments) after every multi-line function signature
- wsl:
- # If true append is only allowed to be cuddled if appending value is
- # matching variables, fields or types on line above. Default is true.
- strict-append: true
- # Allow calls and assignments to be cuddled as long as the lines have any
- # matching variables, fields or types. Default is true.
- allow-assign-and-call: true
- # Allow multiline assignments to be cuddled. Default is true.
- allow-multiline-assign: true
- # Allow declarations (var) to be cuddled.
- allow-cuddle-declarations: false
- # Allow trailing comments in ending of blocks
- allow-trailing-comment: false
- # Force newlines in end of case at this limit (0 = never).
- force-case-trailing-whitespace: 0
+ formats:
+ text:
+ path: stdout
+ print-issued-lines: true
+ print-linter-name: true
linters:
- # please, do not use `enable-all`: it's deprecated and will be removed soon.
- # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
- disable-all: true
+ default: none
enable:
- bodyclose
- depguard
@@ -251,94 +24,109 @@ linters:
- goconst
- gocritic
- godox
- - gofmt
- - goimports
- - golint
- gosec
- - gosimple
- govet
- ineffassign
- - interfacer
- misspell
- nakedret
- - scopelint
- staticcheck
- - structcheck
- - stylecheck
- - typecheck
- unconvert
- unparam
- unused
- whitespace
- # don't enable:
- # - deadcode
- # - gochecknoglobals
- # - gochecknoinits
- # - gocyclo
- # - lll
- # - maligned
- # - prealloc
- # - varcheck
+ - wsl
+ settings:
+ errcheck:
+ check-type-assertions: false
+ check-blank: false
+
+ gosec:
+ exclude:
+ - "G104"
+
+ funlen:
+ lines: 60
+ statements: 40
+
+ govet:
+ enable:
+ - shadow
+
+ gocognit:
+ min-complexity: 20
+
+ dupl:
+ threshold: 100
+
+ goconst:
+ min-len: 3
+ min-occurrences: 3
+
+ misspell:
+ locale: US
+ ignore-rules:
+ - gitlab
+
+ unused:
+ check-exported: false
+
+ unparam:
+ check-exported: false
+
+ nakedret:
+ max-func-lines: 30
+
+ gocritic:
+ settings:
+ captLocal:
+ paramsOnly: true
+
+ godox:
+ keywords:
+ - TODO
+ - BUG
+ - FIXME
+ - NOTE
+ - OPTIMIZE
+ - HACK
+
+ dogsled:
+ max-blank-identifiers: 2
+
+ whitespace:
+ multi-if: false
+ multi-func: false
+
+ wsl:
+ strict-append: true
+ allow-assign-and-call: true
+ allow-multiline-assign: true
+ allow-cuddle-declarations: false
+ allow-trailing-comment: false
+ force-case-trailing-whitespace: 0
+
+ exclusions:
+ rules:
+ - path: _test\.go
+ linters:
+ - gocyclo
+ - errcheck
+ - dupl
+ - gosec
+ - funlen
+ - linters:
+ - lll
+ source: "^//go:generate "
+
+formatters:
+ enable:
+ - gofmt
+ - goimports
+ settings:
+ gofmt:
+ simplify: true
issues:
- # List of regexps of issue texts to exclude, empty list by default.
- # But independently from this option we use default exclude patterns,
- # it can be disabled by `exclude-use-default: false`. To list all
- # excluded by default patterns execute `golangci-lint run --help`
- # exclude:
- # - abcdef
-
- # Excluding configuration per-path, per-linter, per-text and per-source
- exclude-rules:
- # Exclude some linters from running on tests files.
- - path: _test\.go
- linters:
- - gocyclo
- - errcheck
- - dupl
- - gosec
- - funlen
-
- # Exclude known linters from partially hard-vendored code,
- # which is impossible to exclude via "nolint" comments.
- # - path: internal/hmac/
- # text: "weak cryptographic primitive"
- # linters:
- # - gosec
-
- # Exclude some staticcheck messages
- # - linters:
- # - staticcheck
- # text: "SA9003:"
-
- # Exclude lll issues for long lines with go:generate
- - linters:
- - lll
- source: "^//go:generate "
-
- # Independently from option `exclude` we use default exclude patterns,
- # it can be disabled by this option. To list all
- # excluded by default patterns execute `golangci-lint run --help`.
- # Default value for this option is true.
- exclude-use-default: false
-
- # Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
-
- # Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
-
- # Show only new issues: if there are unstaged changes or untracked files,
- # only those changes are analyzed, else only changes in HEAD~ are analyzed.
- # It's a super-useful option for integration of golangci-lint into existing
- # large codebase. It's not practical to fix all existing issues at the moment
- # of integration: much better don't allow issues in new code.
- # Default is false.
new: false
-
- # Show only new issues created after git revision `REV`
- # This should be passed as flag during individual CI jobs.
- # new-from-rev: REV
-
- # Show only new issues created in git patch with set file path.
- # new-from-patch: path/to/patch/file
diff --git a/README.md b/README.md
index aa32642..058ad3d 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,60 @@
-# gin-openapi-validator
\ No newline at end of file
+# gin-openapi-validator
+
+OpenAPI request/response validation middleware for the Gin web framework.
+
+This package plugs into Gin and validates incoming requests and outgoing responses against an OpenAPI 3 specification using `kin-openapi`. It is designed to be lightweight and easy to integrate in existing services.
+
+## Features
+- Validate requests (path params, query params, headers, and body).
+- Validate responses and log validation errors.
+- Supports custom string formats (e.g., UUID RFC 4122).
+- Simple middleware API for Gin.
+
+## Install
+```bash
+go get github.com/phumberdroz/gin-openapi-validator
+```
+
+## Usage
+```go
+package main
+
+import (
+ _ "embed"
+
+ "github.com/gin-gonic/gin"
+ ginopenapivalidator "github.com/phumberdroz/gin-openapi-validator/pkg/gin-openapi-validator"
+)
+
+//go:embed api/openapi.yaml
+var spec []byte
+
+func main() {
+ r := gin.Default()
+ r.Use(ginopenapivalidator.Validator(spec))
+
+ r.GET("/pets", func(c *gin.Context) {
+ c.JSON(200, []gin.H{{"name": "string", "tag": "string", "id": 1}})
+ })
+
+ r.Run(":8080")
+}
+```
+
+## Project Structure
+- `pkg/gin-openapi-validator/`: library source and tests
+- `pkg/gin-openapi-validator/petstore.yaml`: test OpenAPI fixture
+
+## Testing
+```bash
+go test ./...
+```
+
+## Versioning
+This project does not currently publish releases. Consumers should vendor or pin the module version in `go.mod`.
+
+## Contributing
+See `AGENTS.md` for contributor guidelines and local development tips.
+
+## License
+See `LICENSE` for details.
diff --git a/pkg/gin-openapi-validator/validationerror.go b/pkg/gin-openapi-validator/validationerror.go
index b1652eb..92e5dcf 100644
--- a/pkg/gin-openapi-validator/validationerror.go
+++ b/pkg/gin-openapi-validator/validationerror.go
@@ -1,4 +1,5 @@
package ginopenapivalidator
+
// Copied from https://github.com/getkin/kin-openapi/blob/17153345908503543b50b7b6409f9d030bae0beb/openapi3filter/validation_error_encoder.go
// and modified
// Original license is MIT by the authors of kin-openapi
From 2c1f3a791d37d32de91fb92bb30b780a5b452872 Mon Sep 17 00:00:00 2001
From: Pierre Humberdroz
Date: Thu, 12 Mar 2026 20:07:15 +0100
Subject: [PATCH 2/2] Update golangci depguard rules
---
.golangci.yaml | 33 +++++++++------
AGENTS.md | 37 +++++++++++++++++
pkg/gin-openapi-validator/validationerror.go | 29 ++++++++++---
pkg/gin-openapi-validator/validator.go | 43 +++++++++++++-------
pkg/gin-openapi-validator/validator_test.go | 10 +++++
5 files changed, 120 insertions(+), 32 deletions(-)
create mode 100644 AGENTS.md
diff --git a/.golangci.yaml b/.golangci.yaml
index dff1423..64a1f33 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -34,14 +34,14 @@ linters:
- unparam
- unused
- whitespace
- - wsl
+ - wsl_v5
settings:
errcheck:
check-type-assertions: false
check-blank: false
gosec:
- exclude:
+ excludes:
- "G104"
funlen:
@@ -67,9 +67,6 @@ linters:
ignore-rules:
- gitlab
- unused:
- check-exported: false
-
unparam:
check-exported: false
@@ -93,17 +90,29 @@ linters:
dogsled:
max-blank-identifiers: 2
+ depguard:
+ rules:
+ main:
+ list-mode: lax
+ allow:
+ - $gostd
+ - github.com/getkin/kin-openapi
+ - github.com/gin-gonic/gin
+ - github.com/sirupsen/logrus
+ - github.com/stretchr/testify
+ - github.com/phumberdroz/gin-openapi-validator
+
whitespace:
multi-if: false
multi-func: false
- wsl:
- strict-append: true
- allow-assign-and-call: true
- allow-multiline-assign: true
- allow-cuddle-declarations: false
- allow-trailing-comment: false
- force-case-trailing-whitespace: 0
+ wsl_v5:
+ allow-first-in-block: true
+ allow-whole-block: true
+ case-max-lines: 0
+ default: all
+ disable:
+ - assign-expr
exclusions:
rules:
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..a2365f3
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,37 @@
+# Repository Guidelines
+
+## Project Structure & Module Organization
+- `pkg/gin-openapi-validator/` contains the public library code and its tests.
+ - Core middleware and error handling live in `validator.go` and `validationerror.go`.
+ - Package docs are in `doc.go`.
+ - Test fixtures like `petstore.yaml` are stored alongside tests and embedded via `go:embed`.
+- There is no `cmd/` directory; this repo is a library module (`go.mod` defines the module path).
+
+## Build, Test, and Development Commands
+- `go test ./...` runs the full test suite across the module.
+- `go test ./pkg/gin-openapi-validator -run TestBadRequests` runs a focused test by name.
+- `go test -v ./pkg/gin-openapi-validator` runs verbose tests for the main package.
+- `gofmt -w pkg/` formats all Go sources in the package.
+
+## Coding Style & Naming Conventions
+- Use Go standard formatting (tabs, gofmt). Run `gofmt` on changed files.
+- Package name is `ginopenapivalidator` (no underscores); new files should use the same package.
+- Exported identifiers should be PascalCase; unexported helpers should be lowerCamelCase.
+
+## Testing Guidelines
+- Tests use Go’s `testing` package with `testify` assertions (`assert`, `require`).
+- Test files follow the `*_test.go` convention and live next to the code they test.
+- Keep new tests table-driven where it improves coverage (see `TestBadRequests`).
+- Run `go test ./...` before submitting changes.
+
+## Commit & Pull Request Guidelines
+- Git history is minimal and uses short, sentence-case messages (e.g., “Initial commit”).
+ - Keep commit subjects short and descriptive; there is no strict convention enforced.
+- PRs should include:
+ - A brief description of the behavior change.
+ - Any relevant test commands and results (e.g., `go test ./...`).
+ - Sample requests/responses if the middleware behavior changes.
+
+## Security & Configuration Tips
+- The validator loads OpenAPI specs from bytes; avoid committing sensitive API specs.
+- If you add new validation options, keep defaults safe and document any behavior changes.
diff --git a/pkg/gin-openapi-validator/validationerror.go b/pkg/gin-openapi-validator/validationerror.go
index 92e5dcf..b1acce8 100644
--- a/pkg/gin-openapi-validator/validationerror.go
+++ b/pkg/gin-openapi-validator/validationerror.go
@@ -5,7 +5,6 @@ package ginopenapivalidator
// Original license is MIT by the authors of kin-openapi
import (
- "errors"
"fmt"
"net/http"
"strings"
@@ -15,6 +14,8 @@ import (
"github.com/getkin/kin-openapi/routers"
)
+const unsupportedContentTypeReason = "header 'Content-Type' has unexpected value: "
+
// Decode takes a Validation error and decodes back to a *openapi3filter.ValidationError
func Decode(err error) (*openapi3filter.ValidationError, error) {
var cErr *openapi3filter.ValidationError
@@ -23,10 +24,13 @@ func Decode(err error) (*openapi3filter.ValidationError, error) {
Status: http.StatusNotFound,
Title: "not found",
}
+
return cErr, nil
}
+
if e, ok := err.(*routers.RouteError); ok {
cErr = convertRouteError(e)
+
return cErr, nil
}
@@ -48,26 +52,27 @@ func Decode(err error) (*openapi3filter.ValidationError, error) {
if cErr != nil {
return cErr, nil
}
+
return nil, err
}
func convertRouteError(e *routers.RouteError) *openapi3filter.ValidationError {
var cErr *openapi3filter.ValidationError
- fmt.Println(errors.Is(e, routers.ErrPathNotFound))
- //errors.As()
+
switch e.Reason {
case "Path doesn't support the HTTP method":
cErr = &openapi3filter.ValidationError{Status: http.StatusMethodNotAllowed, Title: e.Reason}
default:
cErr = &openapi3filter.ValidationError{Status: http.StatusNotFound, Title: e.Reason}
}
+
return cErr
}
func convertBasicRequestError(e *openapi3filter.RequestError) *openapi3filter.ValidationError {
var cErr *openapi3filter.ValidationError
- unsupportedContentType := "header 'Content-Type' has unexpected value: "
- if strings.HasPrefix(e.Reason, unsupportedContentType) {
+
+ if strings.HasPrefix(e.Reason, unsupportedContentTypeReason) {
if strings.HasSuffix(e.Reason, `: ""`) {
cErr = &openapi3filter.ValidationError{
Status: http.StatusUnsupportedMediaType,
@@ -76,7 +81,7 @@ func convertBasicRequestError(e *openapi3filter.RequestError) *openapi3filter.Va
} else {
cErr = &openapi3filter.ValidationError{
Status: http.StatusUnsupportedMediaType,
- Title: "unsupported content type " + strings.TrimPrefix(e.Reason, unsupportedContentType),
+ Title: "unsupported content type " + strings.TrimPrefix(e.Reason, unsupportedContentTypeReason),
}
}
} else {
@@ -85,11 +90,13 @@ func convertBasicRequestError(e *openapi3filter.RequestError) *openapi3filter.Va
Title: e.Error(),
}
}
+
return cErr
}
func convertErrInvalidRequired(e *openapi3filter.RequestError) *openapi3filter.ValidationError {
var cErr *openapi3filter.ValidationError
+
if e.Reason == openapi3filter.ErrInvalidRequired.Error() && e.Parameter != nil {
cErr = &openapi3filter.ValidationError{
Status: http.StatusBadRequest,
@@ -101,6 +108,7 @@ func convertErrInvalidRequired(e *openapi3filter.RequestError) *openapi3filter.V
Title: e.Error(),
}
}
+
return cErr
}
@@ -139,9 +147,11 @@ func convertParseError(e *openapi3filter.RequestError, innerErr *openapi3filter.
}
}
}
+
if cErr.Title == "" {
cErr.Title = "Could not parse request body"
}
+
return cErr
}
@@ -167,6 +177,7 @@ func convertSchemaError(e *openapi3filter.RequestError, innerErr *openapi3.Schem
cErr.Source = &openapi3filter.ValidationErrorSource{
Parameter: e.Parameter.Name,
}
+
cErr.Title += " See " + cErr.Source.Parameter
} else if innerErr.JSONPointer() != nil {
pointer := innerErr.JSONPointer()
@@ -174,6 +185,7 @@ func convertSchemaError(e *openapi3filter.RequestError, innerErr *openapi3.Schem
cErr.Source = &openapi3filter.ValidationErrorSource{
Pointer: toJSONPointer(pointer),
}
+
cErr.Title += " See " + cErr.Source.Pointer
}
@@ -184,17 +196,22 @@ func convertSchemaError(e *openapi3filter.RequestError, innerErr *openapi3.Schem
for _, enum := range innerErr.Schema.Enum {
enums = append(enums, fmt.Sprintf("%v", enum))
}
+
cErr.Detail = fmt.Sprintf("Value '%v' at %s must be one of: %s",
innerErr.Value, toJSONPointer(innerErr.JSONPointer()), strings.Join(enums, ", "))
+
value := fmt.Sprintf("%v", innerErr.Value)
+
if (e.Parameter.Explode == nil || *e.Parameter.Explode) &&
(e.Parameter.Style == "" || e.Parameter.Style == "form") &&
strings.Contains(value, ",") {
parts := strings.Split(value, ",")
+
cErr.Detail = cErr.Detail + "; " + fmt.Sprintf("perhaps you intended '?%s=%s'",
e.Parameter.Name, strings.Join(parts, "&"+e.Parameter.Name+"="))
}
}
+
return cErr
}
diff --git a/pkg/gin-openapi-validator/validator.go b/pkg/gin-openapi-validator/validator.go
index 3778ce3..29402ec 100644
--- a/pkg/gin-openapi-validator/validator.go
+++ b/pkg/gin-openapi-validator/validator.go
@@ -21,6 +21,10 @@ func (w responseBodyWriter) Write(b []byte) (int, error) {
return w.ResponseWriter.Write(b)
}
+func wrapResponseWriter(c *gin.Context) {
+ c.Writer = &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
+}
+
// ValidatorOptions currently not used but we may use it in the future to add options.
type ValidatorOptions struct {
}
@@ -39,36 +43,35 @@ func Validator(doc []byte, _ ...ValidatorOptions) gin.HandlerFunc {
if err != nil {
panic(err)
}
+
return func(c *gin.Context) {
// Find route
route, pathParams, err := router.FindRoute(c.Request)
if err != nil {
- decodedValidationError, errDecode := Decode(err)
- if errDecode != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
- } else {
- c.AbortWithStatusJSON(decodedValidationError.Status, gin.H{"error": decodedValidationError.Title})
- }
+ abortForValidationError(c, err)
+
return
}
+
requestValidationInput := &openapi3filter.RequestValidationInput{
Request: c.Request,
PathParams: pathParams,
Route: route,
}
+
err = openapi3filter.ValidateRequest(c.Request.Context(), requestValidationInput)
- w := &responseBodyWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
- c.Writer = w
if err != nil {
- decodedValidationError, errDecode := Decode(err)
- if errDecode != nil && decodedValidationError != nil && decodedValidationError.Status != 0 && decodedValidationError.Title != "" {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
- } else {
- c.AbortWithStatusJSON(decodedValidationError.Status, gin.H{"error": decodedValidationError.Title})
- }
+ abortForValidationError(c, err)
+
return
}
+
+ wrapResponseWriter(c)
+
c.Next()
+
+ w := c.Writer.(*responseBodyWriter)
+
responseValidationInput := &openapi3filter.ResponseValidationInput{
RequestValidationInput: requestValidationInput,
Status: c.Writer.Status(),
@@ -81,9 +84,21 @@ func Validator(doc []byte, _ ...ValidatorOptions) gin.HandlerFunc {
if w.body.String() != "" {
responseValidationInput.SetBodyBytes(w.body.Bytes())
}
+
// Validate response.
if err := openapi3filter.ValidateResponse(c.Request.Context(), responseValidationInput); err != nil {
log.WithError(err).Error("could not validate response payload")
}
}
}
+
+func abortForValidationError(c *gin.Context, err error) {
+ decodedValidationError, errDecode := Decode(err)
+ if errDecode != nil || decodedValidationError == nil {
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
+
+ return
+ }
+
+ c.AbortWithStatusJSON(decodedValidationError.Status, gin.H{"error": decodedValidationError.Title})
+}
diff --git a/pkg/gin-openapi-validator/validator_test.go b/pkg/gin-openapi-validator/validator_test.go
index 156aad2..a4d420b 100644
--- a/pkg/gin-openapi-validator/validator_test.go
+++ b/pkg/gin-openapi-validator/validator_test.go
@@ -26,7 +26,9 @@ var s []byte
func TestMain(m *testing.M) {
setupRouter()
+
hook = test.NewGlobal()
+
code := m.Run()
os.Exit(code)
}
@@ -51,6 +53,7 @@ func setupRouter() {
func request(request *http.Request) *httptest.ResponseRecorder {
w := httptest.NewRecorder()
r.ServeHTTP(w, request)
+
return w
}
@@ -72,6 +75,7 @@ func TestPostStatusOk(t *testing.T) {
func TestStatusOkButWrongResponse(t *testing.T) {
defer hook.Reset()
+
req, err := http.NewRequest(http.MethodGet, "/pets/1", nil)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
@@ -86,6 +90,7 @@ func TestStatusOkButWrongResponse(t *testing.T) {
func TestStatusOkUsersUuid(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "/users?userId=bc1a80b7-6e76-4985-be3d-cbf8f8e79a2f", nil)
assert.NoError(t, err)
+
resp := request(req)
assert.Equal(t, http.StatusOK, resp.Code)
}
@@ -162,15 +167,20 @@ func TestBadRequests(t *testing.T) {
testCase := tc
t.Run(testCase.name, func(t *testing.T) {
hook.Reset()
+
req, err := http.NewRequest(testCase.method, testCase.url, bytes.NewBuffer([]byte(testCase.body)))
assert.NoError(t, err)
+
if testCase.setContentType {
req.Header.Set("Content-Type", "application/json")
}
+
resp := request(req)
assert.Equal(t, testCase.expectedStatusCode, resp.Code)
assert.Contains(t, resp.Body.String(), testCase.responseShouldContain)
+
var js json.RawMessage
+
assert.NoError(t, json.Unmarshal(resp.Body.Bytes(), &js))
assert.Len(t, hook.Entries, 0)
})