fix: include Pydantic v2 fields in ValidationError OpenAPI schema (#697)

FastAPI generates the ValidationError schema with only loc, msg, and
type, but Pydantic v2 actually returns input, ctx, and url as well.
Generated clients with strict JSON decoding (Go's DisallowUnknownFields)
cannot parse real 422 responses — the actual validation message gets
replaced by a confusing JSON decoding error.

- Patch the OpenAPI schema in create_app() to add input, ctx, url
- Regenerate spec and Go client
This commit is contained in:
Mr. Khachaturov 2026-03-26 13:25:46 +03:00 committed by GitHub
parent c5700ff5b4
commit 939cb40a73
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 156 additions and 0 deletions

View file

@ -2064,6 +2064,26 @@ def create_app(
# This is required for mounted sub-applications where lifespan may not fire
app.state.memory = memory
# ---------------------------------------------------------------------------
# Patch OpenAPI schema: align ValidationError with Pydantic v2 error format
# ---------------------------------------------------------------------------
# FastAPI auto-generates ValidationError with only loc/msg/type, but Pydantic
# v2 actually returns additional fields: input (the rejected value), ctx (extra
# context dict), and url (link to error docs). Without these in the spec,
# generated clients using strict JSON decoding break on real 422 responses.
_original_openapi = app.openapi
def _patched_openapi() -> dict[str, Any]:
schema = _original_openapi()
ve = schema.get("components", {}).get("schemas", {}).get("ValidationError")
if ve and "input" not in ve.get("properties", {}):
ve["properties"]["input"] = {"title": "Input"}
ve["properties"]["ctx"] = {"title": "Context", "type": "object"}
ve["properties"]["url"] = {"title": "URL", "type": "string"}
return schema
app.openapi = _patched_openapi # type: ignore[assignment]
# Add HTTP metrics middleware
@app.middleware("http")
async def http_metrics_middleware(request, call_next):

View file

@ -3884,12 +3884,18 @@ components:
loc:
- ValidationError_loc_inner
- ValidationError_loc_inner
input: ""
ctx: "{}"
type: type
url: url
- msg: msg
loc:
- ValidationError_loc_inner
- ValidationError_loc_inner
input: ""
ctx: "{}"
type: type
url: url
properties:
detail:
items:
@ -5202,7 +5208,10 @@ components:
loc:
- ValidationError_loc_inner
- ValidationError_loc_inner
input: ""
ctx: "{}"
type: type
url: url
properties:
loc:
items:
@ -5214,6 +5223,13 @@ components:
type:
title: Error Type
type: string
input: {}
ctx:
title: Context
type: object
url:
title: URL
type: string
required:
- loc
- msg

View file

@ -24,6 +24,9 @@ type ValidationError struct {
Loc []ValidationErrorLocInner `json:"loc"`
Msg string `json:"msg"`
Type string `json:"type"`
Input interface{} `json:"input,omitempty"`
Ctx map[string]interface{} `json:"ctx,omitempty"`
Url *string `json:"url,omitempty"`
}
type _ValidationError ValidationError
@ -120,6 +123,103 @@ func (o *ValidationError) SetType(v string) {
o.Type = v
}
// GetInput returns the Input field value if set, zero value otherwise (both if not set or set to explicit null).
func (o *ValidationError) GetInput() interface{} {
if o == nil {
var ret interface{}
return ret
}
return o.Input
}
// GetInputOk returns a tuple with the Input field value if set, nil otherwise
// and a boolean to check if the value has been set.
// NOTE: If the value is an explicit nil, `nil, true` will be returned
func (o *ValidationError) GetInputOk() (*interface{}, bool) {
if o == nil || IsNil(o.Input) {
return nil, false
}
return &o.Input, true
}
// HasInput returns a boolean if a field has been set.
func (o *ValidationError) HasInput() bool {
if o != nil && !IsNil(o.Input) {
return true
}
return false
}
// SetInput gets a reference to the given interface{} and assigns it to the Input field.
func (o *ValidationError) SetInput(v interface{}) {
o.Input = v
}
// GetCtx returns the Ctx field value if set, zero value otherwise.
func (o *ValidationError) GetCtx() map[string]interface{} {
if o == nil || IsNil(o.Ctx) {
var ret map[string]interface{}
return ret
}
return o.Ctx
}
// GetCtxOk returns a tuple with the Ctx field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *ValidationError) GetCtxOk() (map[string]interface{}, bool) {
if o == nil || IsNil(o.Ctx) {
return map[string]interface{}{}, false
}
return o.Ctx, true
}
// HasCtx returns a boolean if a field has been set.
func (o *ValidationError) HasCtx() bool {
if o != nil && !IsNil(o.Ctx) {
return true
}
return false
}
// SetCtx gets a reference to the given map[string]interface{} and assigns it to the Ctx field.
func (o *ValidationError) SetCtx(v map[string]interface{}) {
o.Ctx = v
}
// GetUrl returns the Url field value if set, zero value otherwise.
func (o *ValidationError) GetUrl() string {
if o == nil || IsNil(o.Url) {
var ret string
return ret
}
return *o.Url
}
// GetUrlOk returns a tuple with the Url field value if set, nil otherwise
// and a boolean to check if the value has been set.
func (o *ValidationError) GetUrlOk() (*string, bool) {
if o == nil || IsNil(o.Url) {
return nil, false
}
return o.Url, true
}
// HasUrl returns a boolean if a field has been set.
func (o *ValidationError) HasUrl() bool {
if o != nil && !IsNil(o.Url) {
return true
}
return false
}
// SetUrl gets a reference to the given string and assigns it to the Url field.
func (o *ValidationError) SetUrl(v string) {
o.Url = &v
}
func (o ValidationError) MarshalJSON() ([]byte, error) {
toSerialize,err := o.ToMap()
if err != nil {
@ -133,6 +233,15 @@ func (o ValidationError) ToMap() (map[string]interface{}, error) {
toSerialize["loc"] = o.Loc
toSerialize["msg"] = o.Msg
toSerialize["type"] = o.Type
if o.Input != nil {
toSerialize["input"] = o.Input
}
if !IsNil(o.Ctx) {
toSerialize["ctx"] = o.Ctx
}
if !IsNil(o.Url) {
toSerialize["url"] = o.Url
}
return toSerialize, nil
}

View file

@ -8265,6 +8265,17 @@
"type": {
"type": "string",
"title": "Error Type"
},
"input": {
"title": "Input"
},
"ctx": {
"title": "Context",
"type": "object"
},
"url": {
"title": "URL",
"type": "string"
}
},
"type": "object",