Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to have multiple str in body with an example for each of these str (while typing those value as string) #5249

Closed
9 tasks done
Thytu opened this issue Aug 9, 2022 · 8 comments
Labels
question Question or problem question-migrate

Comments

@Thytu
Copy link

Thytu commented Aug 9, 2022

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

from fastapi import FastAPI, Body
from pydantic import BaseModel

app = FastAPI()


@app.post("/one_body")
def one_body(
    sentence_1: str = Body(
        default=None,
        example="This should works fine",
    ),
):
    return "'I'm one_body"


@app.post("/only_body")
def only_body(
    sentence_1: str = Body(
        default=None,
        example="some example value (1)",
    ),
    sentence_2: str = Body(
        default=None,
        example="some example value (2)",
    ),
):
    return "'I'm only_body"


class Sentence1(BaseModel):
    sentence_2: str

    class Config:
        schema_extra = {
            "example": "This is the first sentence that I have hard-coded",
        }

class Sentence2(BaseModel):
    sentence_2: str

    class Config:
        schema_extra = {
            "example": "This is the second sentence that I have hard-coded",
        }


@app.post("/with_base_model")
def with_base_model(
    sentence_1: Sentence1 = Body(
        default=None,
        example=Sentence1.Config.schema_extra["example"],
    ),
    sentence_2: Sentence2 = Body(
        default=None,
        # example=Sentence2.Config.schema_extra["example"], # NOTE: volontarely comment this line to show that it changes nothing
    ),
):
    return "'I'm only_body"

Description

I'm trying to find a way to :

  • create a route with two str params required in the body
  • those params being typed as str
  • providing an example value for those two str

But seems that if we provide two value in the body we must use BaseModel (cf) to describe it example value, however if we do that the param will no longer be typed as an str but as an object.

How can I create a route requiring two str in the body while providing an example for those two str and still have those values typed as str?

In the example code I create three routes :

  1. /one_body to show that we can provide an example when there is only one value in the body
  2. /only_body to show that when provide two values in the body we can no longer provide an example
  3. /with_base_model to show that when use BaseModel to provide an example, the value is no longer typed as an str
/openapi.json
{
  "openapi": "3.0.2",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "paths": {
    "/one_body": {
      "post": {
        "summary": "One Body",
        "operationId": "one_body_one_body_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "title": "Sentence 1",
                "type": "string"
              },
              "example": "This should works fine"
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/only_body": {
      "post": {
        "summary": "Only Body",
        "operationId": "only_body_only_body_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Body_only_body_only_body_post"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    },
    "/with_base_model": {
      "post": {
        "summary": "With Base Model",
        "operationId": "with_base_model_with_base_model_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Body_with_base_model_with_base_model_post"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {
                  
                }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HTTPValidationError"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Body_only_body_only_body_post": {
        "title": "Body_only_body_only_body_post",
        "type": "object",
        "properties": {
          "sentence_1": {
            "title": "Sentence 1",
            "type": "string"
          },
          "sentence_2": {
            "title": "Sentence 2",
            "type": "string"
          }
        }
      },
      "Body_with_base_model_with_base_model_post": {
        "title": "Body_with_base_model_with_base_model_post",
        "type": "object",
        "properties": {
          "sentence_1": {
            "$ref": "#/components/schemas/Sentence1"
          },
          "sentence_2": {
            "$ref": "#/components/schemas/Sentence2"
          }
        }
      },
      "HTTPValidationError": {
        "title": "HTTPValidationError",
        "type": "object",
        "properties": {
          "detail": {
            "title": "Detail",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ValidationError"
            }
          }
        }
      },
      "Sentence1": {
        "title": "Sentence1",
        "required": [
          "sentence_2"
        ],
        "type": "object",
        "properties": {
          "sentence_2": {
            "title": "Sentence 2",
            "type": "string"
          }
        },
        "example": "This is the first sentence that I have hard-coded"
      },
      "Sentence2": {
        "title": "Sentence2",
        "required": [
          "sentence_2"
        ],
        "type": "object",
        "properties": {
          "sentence_2": {
            "title": "Sentence 2",
            "type": "string"
          }
        },
        "example": "This is the second sentence that I have hard-coded"
      },
      "ValidationError": {
        "title": "ValidationError",
        "required": [
          "loc",
          "msg",
          "type"
        ],
        "type": "object",
        "properties": {
          "loc": {
            "title": "Location",
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "integer"
                }
              ]
            }
          },
          "msg": {
            "title": "Message",
            "type": "string"
          },
          "type": {
            "title": "Error Type",
            "type": "string"
          }
        }
      }
    }
  }
}

Operating System

Linux

Operating System Details

No response

FastAPI Version

0.79.0

Python Version

Python 3.8.10

Additional Context

No response

@Thytu Thytu added the question Question or problem label Aug 9, 2022
@JarroVGIT
Copy link
Contributor

I thought this was a straight up answer but I ran into several weird behaviours when trying to come up with examples. I need a little more time to really dive into this, because it is not at all consistent. For starters, try replacing your Sentence1 class with this:

class Sentence1(BaseModel):
    sentence_1: str
    foo: str

    class Config:
        schema_extra = {
            "examples": [
                {
                    "sentence_1": "This is the first sentence that I have hard-coded",
                    "foo": "bar",
                }
            ]
        }

This is not picked up at all, while using just 'example' is picked up. That is even the case when you have multiple properties (sentence_1 and foo in this case). Weird and unexpected.

@Thytu
Copy link
Author

Thytu commented Aug 10, 2022

This is not picked up at all, while using just 'example' is picked up. That is even the case when you have multiple properties (sentence_1 and foo in this case). Weird and unexpected.

Yea I encountered this behaviour (in part leading me to split the params into multiple schemas)

Thanks for your time, curious to know where the final point of that will be

@JarroVGIT
Copy link
Contributor

Alright so this is partly a SwaggerUI problem I think, and partly a FastAPI problem. Let's talk about the SwaggerUI problems first. The example that I gave above runs properly in /redoc but will fail in SwaggerUI.

Code:

class Sentence3(BaseModel):
    sentence_1: str
    sentence_2: str

    class Config:
        schema_extra = {
            "examples": [
                {
                    "sentence_1": "Hello sentence 1 from model3",
                    "sentence_2": "Hello sentence 2 from model3",
                },
            ]
        }


@app.post("/with_model_3")
def with_model_3(sentences: Sentence3):
    return "hello there"

This will render in SwaggerUI:
image

but will render properly in Redoc:
image

I came on to this because I couldn't find anything wrong with the generated openapi.json (although example will be deprecated in 3.1.x, but FastAPI is using 3.0.2). When I checked /redoc I was relieved that I was not going crazy completely ;)

So at least, you don't have to separate the two sentences in two BaseModel subclasses, you can put it in one class (like I did above).

Now, the weird stuff is still that when you put two Body() parameters of type str, FastAPI does bundle those into one object (with two strings) but forgoes the example that you gave with it. I found that this is caused by get_body_field() in dependencies/utils.py. When a route is created (e.g. with @app.post()), the body field is generated. In this case, it is based on two separate string. APIRoute init calls get_body_field(), which in turn calls create_response_field(). However, in get_body_field(), it seems that it drops any additional information on the fields (not just example, like everything that the Body() parameter has on it. See code from this line onwards. I would say this is a bug.

@Thytu
Copy link
Author

Thytu commented Aug 17, 2022

Thanks for you answer @JarroVGIT.

It seems that Form neither produce an example value in the /openapi.json.
I guess it's related, isn't it?

Example code

from fastapi import Body, FastAPI, Form

app = FastAPI()

@app.post("/body/empty")
async def body_empty(importance: int = Body()):
    return ""

# Does provide an example value
@app.post("/body/example")
async def body_example(importance: int = Body(example=2)):
    return ""


@app.post("/form/empty")
async def form_empty(importance: int = Form()):
    return ""

# Does NOT provide an example value
@app.post("/form/example")
async def form_example(importance: int = Form(example=2)):
    return ""

@JarroVGIT
Copy link
Contributor

Body() is a special function that returns an instance of class Body, the same goes for Form() (which returns an instance of Form). The class Form is a subclass of Body, so I would expect the same behaviour indeed.

@Thytu
Copy link
Author

Thytu commented Aug 22, 2022

@JarroVGIT I will take a look at starting from today.

Keeping you up to date during this week.

@Thytu
Copy link
Author

Thytu commented Aug 23, 2022

@JarroVGIT This is my current stat of understanding regarding the code base :

  1. get_param_field is called by get_dependant (for each signature_params) which call create_response_field.

  2. create_response_field returns field that contains the example value (field.field_info.example) for the parameter.

  3. create_response_field is also called to create the route schema in get_body_field returning final_field.

  4. final_field does not seems to contain any informations relating to the body parameters, and so to the example values.
    (Does I miss-understand this part? If so, where does the information concerning the body parameters are provided and stored within final_field?)

If final_field does indeed not contains any informations relating to the body parameters I need to search wether this bug is due to :
A. The ApiRoute not providing those informations when creating the openapi.json
B. The ApiRoute does provide those informations when creating the openapi.json but there are not used.

On the contrary, if final_field contains the informations relating to the body parameters (and thus the example values) I need to verify that those values are keep in final_field.

I have also few questions for you:

  • Where does the openapi.json is create ?
  • Is final_field used to create the schema for each param in the openapi.json or is only used to create the path schema and self.dependencies is used to create those schema?

I hope my explanations and understanding are not too messy.
Thanks for your feedbacks.

@Thytu
Copy link
Author

Thytu commented Aug 23, 2022

Update :

As you do (cf):

for f in flat_dependant.body_params:
    BodyModel.__fields__[f.name] = f

followed by (cf):

final_field = create_response_field(
    name="body",
    type_=BodyModel, # used here
    required=required,
    alias="body",
    field_info=BodyFieldInfo(**BodyFieldInfo_kwargs),
)

It seems that you do provides to final_field the informations about the parameters and their example values.

The question now is, do you rely on final_field to create the schema for each parameter or do you rely on self.dependencies?

If you rely on final_field to create the schema for each parameter, this mean that the issue may comes from pydantic rather than from FastAPI (as ModelField comes from pydantic and ModelField contains those informations in type_).

Repository owner locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #8365 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
3 participants