Repository: luolingchun/flask-openapi3

Issue:
ValidationError schema in OpenAPI docs doesn't match actual Pydantic responses
# Question: ValidationError schema in OpenAPI docs doesn't match actual Pydantic responses - is this expected?

Hi! I'm new to Flask-OpenAPI3 and Pydantic, and I'm confused about something I'm seeing with validation error responses. I'm not sure if I'm doing something wrong or if this is expected behavior.

## What I'm observing

When I look at the OpenAPI documentation generated by Flask-OpenAPI3, the validation error schema shows this format:

```json
[
  {
    "ctx": null,
    "loc": null,
    "msg": null,
    "type_": null
  }
]
```

But when I actually trigger a validation error by sending invalid data, I get responses that look like this:

```json
[
  {
    "type": "missing",
    "loc": ["email"],
    "msg": "Field required",
    "input": { "invalid_field": "test" },
    "url": "https://errors.pydantic.dev/2.11/v/missing"
  }
]
```

## My setup

I'm using:

- `flask-openapi3[swagger]==4.2.1`
- `pydantic==2.11.0`

Here's my minimal example:

```python
from flask_openapi3.openapi import OpenAPI
from pydantic import BaseModel

app = OpenAPI(__name__)

class LoginRequest(BaseModel):
    email: str
    password: str

@app.post("/login")
def login(body: LoginRequest):
    """Login endpoint that expects email and password"""
    return {"message": f"Login successful for {body.email}"}
```

## My questions

1. **Is this the expected behavior?** Should the OpenAPI schema match the actual validation error format?

2. **Am I missing some configuration?** Do I need to configure `validation_error_model` or `validation_error_callback` to make them match?

3. **Is this related to Pydantic v2?** I noticed the actual errors have fields like `input` and `url` that seem to be from Pydantic v2, but the schema has different field names.

4. **How should I handle this for API consumers?** If the documentation doesn't match the actual responses, how should frontend developers know what to expect?

## What I've tried

I've looked at the documentation for `validation_error_status`, `validation_error_model`, and `validation_error_callback`, but I'm not sure how to use them properly to fix this discrepancy.

  

Any guidance would be really appreciated! I want to make sure I understand how this is supposed to work before building my API further.

Thanks!

Discussion hints:

Solution patch:
diff --git a/flask_openapi3/models/validation_error.py b/flask_openapi3/models/validation_error.py
index d36f9b7..961788f 100644
--- a/flask_openapi3/models/validation_error.py
+++ b/flask_openapi3/models/validation_error.py
@@ -7,14 +7,16 @@ from pydantic import BaseModel, Field
 
 
 class ValidationErrorModel(BaseModel):
-    # More information: https://docs.pydantic.dev/latest/usage/models/#error-handling
-    loc: Optional[list[str]] = Field(None, title="Location", description="the error's location as a list. ")
-    msg: Optional[str] = Field(None, title="Message", description="a computer-readable identifier of the error type.")
-    type_: Optional[str] = Field(None, title="Error Type", description="a human readable explanation of the error.")
+    # More information: https://docs.pydantic.dev/latest/concepts/models/#error-handling
+    type: str = Field(..., title="Error Type", description="A computer-readable identifier of the error type.")
+    loc: list[Any] = Field(..., title="Location", description="The error's location as a list.")
+    msg: str = Field(..., title="Message", description="A human readable explanation of the error.")
+    input: Any = Field(..., title="Input", description="The input provided for validation.")
+    url: Optional[str] = Field(None, title="URL", description="The URL to further information about the error.")
     ctx: Optional[dict[str, Any]] = Field(
         None,
         title="Error context",
-        description="an optional object which contains values required to render the error message."
+        description="An optional object which contains values required to render the error message.",
     )
 
 


Test patch:
diff --git a/tests/test_pydantic_validation_error.py b/tests/test_pydantic_validation_error.py
new file mode 100644
index 0000000..56aa89f
--- /dev/null
+++ b/tests/test_pydantic_validation_error.py
@@ -0,0 +1,101 @@
+import pytest
+from pydantic import BaseModel, Field
+
+from flask_openapi3 import OpenAPI
+
+app = OpenAPI(__name__)
+app.config["TESTING"] = True
+
+
+@pytest.fixture
+def client():
+    client = app.test_client()
+    return client
+
+
+class LoginRequest(BaseModel):
+    email: str = Field(..., description="User email")
+    password: str = Field(..., description="User password")
+
+
+@app.post("/login")
+def login(body: LoginRequest):
+    return {"message": f"Login successful for {body.email}"}
+
+
+def test_pydantic_validation_error_schema(client):
+    resp = client.get("/openapi/openapi.json")
+    assert resp.status_code == 200
+
+    schemas = resp.json["components"]["schemas"]
+    assert "ValidationErrorModel" in schemas
+
+    validation_error_schema = schemas["ValidationErrorModel"]
+    properties = validation_error_schema["properties"]
+
+    assert "type" in properties
+    assert "loc" in properties
+    assert "msg" in properties
+    assert "input" in properties
+    assert "url" in properties
+    assert "ctx" in properties
+
+    assert properties["type"]["type"] == "string"
+    assert properties["loc"]["type"] == "array"
+    assert properties["msg"]["type"] == "string"
+
+    required_fields = validation_error_schema.get("required", [])
+    assert "type" in required_fields
+    assert "loc" in required_fields
+    assert "msg" in required_fields
+    assert "input" in required_fields
+    assert "ctx" not in required_fields
+
+
+def test_pydantic_validation_error_response(client):
+    resp = client.post(
+        "/login",
+        json={"invalid_field": "test"},
+        content_type="application/json",
+    )
+
+    assert resp.status_code == 422
+
+    errors = resp.json
+    assert isinstance(errors, list)
+    assert len(errors) > 0
+
+    error = errors[0]
+    assert "type" in error
+    assert "loc" in error
+    assert "msg" in error
+    assert "input" in error
+
+    assert isinstance(error["type"], str)
+    assert isinstance(error["loc"], list)
+    assert isinstance(error["msg"], str)
+    assert error["type"] in ["missing", "string_type", "value_error", "extra_forbidden"]
+
+
+def test_pydantic_missing_field_error_response(client):
+    resp = client.post(
+        "/login",
+        json={},
+        content_type="application/json",
+    )
+
+    assert resp.status_code == 422
+
+    errors = resp.json
+    assert len(errors) >= 2
+
+    email_error = None
+    for error in errors:
+        if error.get("loc") == ["email"]:
+            email_error = error
+            break
+
+    assert email_error is not None
+    assert email_error["type"] == "missing"
+    assert email_error["msg"] == "Field required"
+    assert email_error["input"] == {}