Repository: tiangolo/fastapi

Issue:
Bad `loc` on validation error, if payload represended by one model
### Describe the bug

Really like your framework, but there is, indeed, an annoying issue with `loc` on validation error with one object as payload.

### To Reproduce
Code sample
```Python
from typing import List

from fastapi import FastAPI, Body
from pydantic import BaseModel
app = FastAPI()

class NameModel(BaseModel):
    name: str

@app.post("/test", response_model=NameModel)
def test(obj: NameModel, ):  # bad
    return obj

@app.post("/test_embed", response_model=NameModel)
def test(obj: NameModel = Body(..., embed=True)):  # ok
    return obj

@app.post("/test_multiple", response_model=List[NameModel])
def test(obj1: NameModel, obj2: NameModel):  # ok
    return obj1, obj2
```

When you make a request to endpoint (`/test`) with the wrong payload (e.g.: `{}`), it always includes the variable name into error location, despite it has no relation to request.

It makes no sense, moreover, it complicates the logic for error printing on fronted, because they just don't know and not required to know the name of the backend`s internal variable.

```json
{
    "detail": [
        {
            "loc": [
                "body",
                "obj",
                "name"
            ],
            "msg": "field required",
            "type": "value_error.missing"
        }
    ]
}
```
it should be
```json
{
    "detail": [
        {
            "loc": [
                "body",
                "name"
            ],
            "msg": "field required",
            "type": "value_error.missing"
        }
    ]
}
```

With the embedded object (`/test_embed`) or multiple objects (`/test_multiple`), it works as expected, putting the variable name into location, because it should be in the payload.

### Expected behavior

Don't include the variable name into location error, if it is not reflected in schema / not embedded / not expected to be in payload.

### Environment

- OS: macOS
- FastAPI 0.52.0
- Python 3.6.8



Discussion:
We can also observe this behaviour, it caused a bit of head-scratching when trying to diagnose the source of an error between frontend and backend services.
Also observed this behaviour when fiddling around with the framework for the first time.

Resolving patch:
diff --git a/fastapi/dependencies/utils.py b/fastapi/dependencies/utils.py
--- a/fastapi/dependencies/utils.py
+++ b/fastapi/dependencies/utils.py
@@ -623,9 +623,17 @@ async def request_body_to_args(
         field = required_params[0]
         field_info = get_field_info(field)
         embed = getattr(field_info, "embed", None)
-        if len(required_params) == 1 and not embed:
+        field_alias_omitted = len(required_params) == 1 and not embed
+        if field_alias_omitted:
             received_body = {field.alias: received_body}
+
         for field in required_params:
+            loc: Tuple[str, ...]
+            if field_alias_omitted:
+                loc = ("body",)
+            else:
+                loc = ("body", field.alias)
+
             value: Any = None
             if received_body is not None:
                 if (
@@ -636,7 +644,7 @@ async def request_body_to_args(
                     try:
                         value = received_body.get(field.alias)
                     except AttributeError:
-                        errors.append(get_missing_field_error(field.alias))
+                        errors.append(get_missing_field_error(loc))
                         continue
             if (
                 value is None
@@ -648,7 +656,7 @@ async def request_body_to_args(
                 )
             ):
                 if field.required:
-                    errors.append(get_missing_field_error(field.alias))
+                    errors.append(get_missing_field_error(loc))
                 else:
                     values[field.name] = deepcopy(field.default)
                 continue
@@ -667,7 +675,9 @@ async def request_body_to_args(
                 awaitables = [sub_value.read() for sub_value in value]
                 contents = await asyncio.gather(*awaitables)
                 value = sequence_shape_to_type[field.shape](contents)
-            v_, errors_ = field.validate(value, values, loc=("body", field.alias))
+
+            v_, errors_ = field.validate(value, values, loc=loc)
+
             if isinstance(errors_, ErrorWrapper):
                 errors.append(errors_)
             elif isinstance(errors_, list):
@@ -677,12 +687,12 @@ async def request_body_to_args(
     return values, errors
 
 
-def get_missing_field_error(field_alias: str) -> ErrorWrapper:
+def get_missing_field_error(loc: Tuple[str, ...]) -> ErrorWrapper:
     if PYDANTIC_1:
-        missing_field_error = ErrorWrapper(MissingError(), loc=("body", field_alias))
+        missing_field_error = ErrorWrapper(MissingError(), loc=loc)
     else:  # pragma: no cover
         missing_field_error = ErrorWrapper(  # type: ignore
-            MissingError(), loc=("body", field_alias), config=BaseConfig,
+            MissingError(), loc=loc, config=BaseConfig,
         )
     return missing_field_error
 


Test patch:
diff --git a/tests/test_multi_body_errors.py b/tests/test_multi_body_errors.py
--- a/tests/test_multi_body_errors.py
+++ b/tests/test_multi_body_errors.py
@@ -104,7 +104,7 @@ def save_item_no_body(item: List[Item]):
     "detail": [
         {
             "ctx": {"limit_value": 0.0},
-            "loc": ["body", "item", 0, "age"],
+            "loc": ["body", 0, "age"],
             "msg": "ensure this value is greater than 0",
             "type": "value_error.number.not_gt",
         }
@@ -114,22 +114,22 @@ def save_item_no_body(item: List[Item]):
 multiple_errors = {
     "detail": [
         {
-            "loc": ["body", "item", 0, "name"],
+            "loc": ["body", 0, "name"],
             "msg": "field required",
             "type": "value_error.missing",
         },
         {
-            "loc": ["body", "item", 0, "age"],
+            "loc": ["body", 0, "age"],
             "msg": "value is not a valid decimal",
             "type": "type_error.decimal",
         },
         {
-            "loc": ["body", "item", 1, "name"],
+            "loc": ["body", 1, "name"],
             "msg": "field required",
             "type": "value_error.missing",
         },
         {
-            "loc": ["body", "item", 1, "age"],
+            "loc": ["body", 1, "age"],
             "msg": "value is not a valid decimal",
             "type": "type_error.decimal",
         },
diff --git a/tests/test_tutorial/test_body/test_tutorial001.py b/tests/test_tutorial/test_body/test_tutorial001.py
--- a/tests/test_tutorial/test_body/test_tutorial001.py
+++ b/tests/test_tutorial/test_body/test_tutorial001.py
@@ -92,7 +92,7 @@ def test_openapi_schema():
 price_missing = {
     "detail": [
         {
-            "loc": ["body", "item", "price"],
+            "loc": ["body", "price"],
             "msg": "field required",
             "type": "value_error.missing",
         }
@@ -102,7 +102,7 @@ def test_openapi_schema():
 price_not_float = {
     "detail": [
         {
-            "loc": ["body", "item", "price"],
+            "loc": ["body", "price"],
             "msg": "value is not a valid float",
             "type": "type_error.float",
         }
@@ -112,12 +112,12 @@ def test_openapi_schema():
 name_price_missing = {
     "detail": [
         {
-            "loc": ["body", "item", "name"],
+            "loc": ["body", "name"],
             "msg": "field required",
             "type": "value_error.missing",
         },
         {
-            "loc": ["body", "item", "price"],
+            "loc": ["body", "price"],
             "msg": "field required",
             "type": "value_error.missing",
         },
@@ -126,11 +126,7 @@ def test_openapi_schema():
 
 body_missing = {
     "detail": [
-        {
-            "loc": ["body", "item"],
-            "msg": "field required",
-            "type": "value_error.missing",
-        }
+        {"loc": ["body"], "msg": "field required", "type": "value_error.missing",}
     ]
 }
 
diff --git a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py
--- a/tests/test_tutorial/test_body_nested_models/test_tutorial009.py
+++ b/tests/test_tutorial/test_body_nested_models/test_tutorial009.py
@@ -95,7 +95,7 @@ def test_post_invalid_body():
     assert response.json() == {
         "detail": [
             {
-                "loc": ["body", "weights", "__key__"],
+                "loc": ["body", "__key__"],
                 "msg": "value is not a valid integer",
                 "type": "type_error.integer",
             }
diff --git a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
--- a/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
+++ b/tests/test_tutorial/test_custom_request_and_route/test_tutorial002.py
@@ -18,7 +18,7 @@ def test_exception_handler_body_access():
             "body": '{"numbers": [1, 2, 3]}',
             "errors": [
                 {
-                    "loc": ["body", "numbers"],
+                    "loc": ["body"],
                     "msg": "value is not a valid list",
                     "type": "type_error.list",
                 }
diff --git a/tests/test_tutorial/test_handling_errors/test_tutorial005.py b/tests/test_tutorial/test_handling_errors/test_tutorial005.py
--- a/tests/test_tutorial/test_handling_errors/test_tutorial005.py
+++ b/tests/test_tutorial/test_handling_errors/test_tutorial005.py
@@ -92,7 +92,7 @@ def test_post_validation_error():
     assert response.json() == {
         "detail": [
             {
-                "loc": ["body", "item", "size"],
+                "loc": ["body", "size"],
                 "msg": "value is not a valid integer",
                 "type": "type_error.integer",
             }