Skip to content

✨ Add custom GenerateJsonSchema#9873

Closed
Kludex wants to merge 4 commits intomasterfrom
feat/custom-generate-json-schema
Closed

✨ Add custom GenerateJsonSchema#9873
Kludex wants to merge 4 commits intomasterfrom
feat/custom-generate-json-schema

Conversation

@Kludex
Copy link
Copy Markdown
Member

@Kludex Kludex commented Jul 13, 2023

I need to fix the tests... 👀

@dmontagu
Copy link
Copy Markdown
Contributor

Also need to add tests of this change since it seems none got broken.

The following prints something that changed:

import json

from fastapi import FastAPI

app = FastAPI()


@app.get('/')
def get_stuff(x: str | None = None):
    return {'x': x}


print(json.dumps(app.openapi(), indent=2))

but obviously needs a real test

@fnep
Copy link
Copy Markdown

fnep commented Aug 2, 2023

I test this using python3 -m pip install --force-reinstall git+https://github.com/tiangolo/fastapi/@feat/custom-generate-json-schema and it fixes at least my issue, from #9709 (comment). It would be very nice to have this in the next FastAPI release.

@matt7aylor
Copy link
Copy Markdown

Thanks for the work on this. This PR does resolve issues with removing the extra null type from the parameters in the schema, which is useful and fixes some issues. However, you do still get superfluous null types for optional parameters in response models etc., which also complicates the resulting openapi schema introducing unnecessary anyOf etc.

In the below example, optional in the parameter schema comes out fine (using this PR) but the response model still has the extra | null. This can be manually avoided using the method detailed in pydantic/pydantic#6653 as shown for optional_better, which results in a much nicer response schema. It would be great if something like this could also be implemented in the custom schema generator so that it isn't necessary to add this pydantic fiddle to every optional element of every model.

(For reference: FastAPI before pydantic v2 with Optional[str] = None produces schema without the extra | null)

from fastapi import FastAPI
from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema

app = FastAPI()


class Model(BaseModel):
    required: str
    optional: str | None = None
    optional_better: str | SkipJsonSchema[None] = Field(
        default=None, json_schema_extra=lambda x: x.pop("default")
    )  # From https://github.com/pydantic/pydantic/pull/6653#issuecomment-1646211152


@app.get("/echo/{required}", response_model=Model, response_model_exclude_none=True)
def echo(required: str, optional: str | None = None, optional_better: str | None = None):
        return Model(required=required, optional=optional, optional_better=optional_better)

image

@pythonweb2
Copy link
Copy Markdown

Finally found the issue here, I opened this discussion with the same issue.

@pythonweb2
Copy link
Copy Markdown

@tiangolo from what I can see, this doesn't seem like a new feature, since the schema generation with Pydantic v2 is not the same as v1, and is not correct from what I can see from the OpenAPI spec.

https://swagger.io/docs/specification/data-models/data-types/

Note that there is no null type; instead, the nullable attribute is used as a modifier of the base type.

@caffeinatedMike
Copy link
Copy Markdown

Any recent news on this much-needed feature?

@caffeinatedMike
Copy link
Copy Markdown

For anyone who is still encountering this problem that stumbles upon this stalled PR, the below snippet can be used for both FastAPI router parameters and Pydantic models.

from typing import TYPE_CHECKING, Annotated, Generic, List, TypeVar, Union

from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema

if TYPE_CHECKING:
    from typing import Optional as OptionalParam
else:
    class OptionalParam:
        """Hold-over typing solution for cleaner Swagger UI rendering.

        Accomplishes two things:
        1) Optional typing within Annotated that appeases MyPy
           while properly showing endpoint parameter types in the Swagger UI.
        2) Eliminates the ugly [type, null] arrays shown for Schemas in the Swagger UI,
           simply showing optional fields without the red asterisk.

        Credit: https://github.com/pydantic/pydantic/issues/6647#issuecomment-1646227125
        FastAPI Solution Pending: https://github.com/tiangolo/fastapi/pull/9873

        Usage within a Pydantic model ::

            class BetterOptional(BaseModel):
                x: OptionalParam[str] = Field(
                    None, description="No array of `[str, null]` in UI"
                )
                y: OptionalParam[int]

        Usage within FastAPI view function parameters ::

            @api.get("/d", response_model=BetterOptional)
            async def d(
                x: Annotated[OptionalParam[str], Query()] = None,
                y: Annotated[OptionalParam[int], Query()] = None,
                z: Annotated[OptionalParam[bool], Query()] = False
            ):
                '''Temporary fix.'''
        """

        def __class_getitem__(cls, item):
            return Annotated[
                Union[item, SkipJsonSchema[None]],
                Field(json_schema_extra=lambda x: x.pop("default", None))
            ]

@mkokotovich
Copy link
Copy Markdown

I am also interested in a solution to this problem

@heralight
Copy link
Copy Markdown

A generic way for me to suppress this ugly anyof null that break openapi generator was to rewrite the fastapi scheme output like:

def handle_anyof_nullable(schema: dict):
    """Recursively modifies the schema to handle anyOf with null for OpenAPI 3.0 compatibility."""

    if isinstance(schema, dict):
        for key, value in list(
            schema.items()
        ):  # Iterate over a copy to avoid modification errors
            if key == "anyOf" and isinstance(value, list):
                non_null_types = [item for item in value if item.get("type") != "null"]
                if len(value) > len(non_null_types):  # Found 'null' in anyOf
                    if len(non_null_types) == 1:
                        schema.update(non_null_types[0])  # Replace with non-null type
                        schema["nullable"] = True
                        del schema[key]  # Remove anyOf
                    else:
                        logger.warning(
                            f"Complex anyOf with multiple non-null types at key '{key}'. Review manually."
                        )
            else:
                handle_anyof_nullable(value)
    elif isinstance(schema, list):
        for item in schema:
            handle_anyof_nullable(item)


def downgrade_openapi_schema_to_3_0(schema: dict) -> dict:
    """Downgrades an OpenAPI schema from 3.1 to 3.0, handling anyOf with null."""

    logger.info("Customizing OpenAPI schema for 3.0 compatibility...")
    handle_anyof_nullable(schema)
    return schema


def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    openapi_schema = get_openapi(
        title="my API",
        openapi_version="3.0.1",
        version="1.0.0",
        description="my OpenAPI schema",
        routes=app.routes,
    )
    # Here, modify the openapi_schema as needed to ensure 3.0.0 compatibility
    # For example, adjust for `nullable` fields compatibility
    openapi_schema = downgrade_openapi_schema_to_3_0(openapi_schema)
    app.openapi_schema = openapi_schema
    return app.openapi_schema


app.openapi = custom_openapi

@Bue-von-hon
Copy link
Copy Markdown

Thanks for the work on this. This PR does resolve issues with removing the extra null type from the parameters in the schema, which is useful and fixes some issues. However, you do still get superfluous null types for optional parameters in response models etc., which also complicates the resulting openapi schema introducing unnecessary anyOf etc.

In the below example, optional in the parameter schema comes out fine (using this PR) but the response model still has the extra | null. This can be manually avoided using the method detailed in pydantic/pydantic#6653 as shown for optional_better, which results in a much nicer response schema. It would be great if something like this could also be implemented in the custom schema generator so that it isn't necessary to add this pydantic fiddle to every optional element of every model.

(For reference: FastAPI before pydantic v2 with Optional[str] = None produces schema without the extra | null)

from fastapi import FastAPI
from pydantic import BaseModel, Field
from pydantic.json_schema import SkipJsonSchema

app = FastAPI()


class Model(BaseModel):
    required: str
    optional: str | None = None
    optional_better: str | SkipJsonSchema[None] = Field(
        default=None, json_schema_extra=lambda x: x.pop("default")
    )  # From https://github.com/pydantic/pydantic/pull/6653#issuecomment-1646211152


@app.get("/echo/{required}", response_model=Model, response_model_exclude_none=True)
def echo(required: str, optional: str | None = None, optional_better: str | None = None):
        return Model(required=required, optional=optional, optional_better=optional_better)

image

@matt7aylor This is a nice workaround.
It also works perfectly when declaring properties of the request modle.
Since postman doesn't support anyOf syntax for shemas, we need to remove nulls from shemas of types declared as optional.
This supports it perfectly.

@maurerle
Copy link
Copy Markdown

maurerle commented Jun 6, 2024

I just tried this PR with this example:

from typing import Annotated
from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items/")
async def read_items(q: Annotated[list[str] | None, Query()] = None):
    query_items = {"q": q}
    return query_items

@app.get("/item1/")
async def read_items2(q: list[int] | None = Query(default=None)):
    query_items = {"q": q}
    return query_items

and got:
AttributeError: property 'mode' of 'GenerateJsonSchema' object has no setter

@maurerle
Copy link
Copy Markdown

maurerle commented Jun 6, 2024

Also taking a look at this summary of related issues:
#10836 (comment)

Copy link
Copy Markdown

@maurerle maurerle left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, except for the _mode access.
Can you fix this and rebase onto master? :)

Comment thread fastapi/_compat.py
)

for key, mode, schema in inputs:
self.mode = mode
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.mode = mode
self._mode = mode

The mode itself does not have a setter

Comment thread fastapi/_compat.py

json_schemas_map: dict[tuple[JsonSchemaKeyT, JsonSchemaMode], DefsRef] = {}
for key, mode, schema in inputs:
self.mode = mode
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self.mode = mode
self._mode = mode

The mode itself does not have a setter

@jordantshaw
Copy link
Copy Markdown

Any progress on this?

@github-actions github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Sep 5, 2025
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Sep 5, 2025

This pull request has a merge conflict that needs to be resolved.

@tiangolo
Copy link
Copy Markdown
Member

Thanks for the effort with this!

I just added support for both Pydantic v2 and pydantic.v1 models, to help people migrate to v2 here: #14168

But as GenerateJsonSchema is just not compatible with v1 models (and there's no straightforward way to make it compatible, I really tried 😅), I had to write a lot of extra custom code, copying parts from v1 and making them compatible with v2 models, etc. So, having a custom GenerateJsonSchema is not gonna be feasible for now (and until v1 is removed here).

So, for now, I'll close this one, but thanks @Kludex and everyone here! 🍰

@tiangolo tiangolo closed this Oct 11, 2025
@tiangolo tiangolo deleted the feat/custom-generate-json-schema branch October 11, 2025 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conflicts Automatically generated when a PR has a merge conflict feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.