Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@MaxwellPayne
Copy link
Contributor

Problem

Generating openapi specs for models with a formfeed (\f) in the docstring yields inconsistent results. For example, in the case of:

class Address(BaseModel):
    """
    This is a public description of an Address
    \f
    You can't see this part of the docstring, it's private!
    """
    ...

Sometimes the openapi description of Address correctly renders as This is a public description of an Address\n. But other times, you get the entire docstring even though the \f should be keeping the remainder of the docstring private.

The bug seemed to be in the following function:

def get_model_definitions(
    *,
    flat_models: Set[Union[Type[BaseModel], Type[Enum]]],
    model_name_map: Dict[Union[Type[BaseModel], Type[Enum]], str],
) -> Dict[str, Any]:
    definitions: Dict[str, Dict[str, Any]] = {}
    for model in flat_models:
        m_schema, m_definitions, m_nested_models = model_process_schema(
            model, model_name_map=model_name_map, ref_prefix=REF_PREFIX
        )
        definitions.update(m_definitions)
        model_name = model_name_map[model]
        if "description" in m_schema:
            m_schema["description"] = m_schema["description"].split("\f")[0]
        definitions[model_name] = m_schema
    return definitions

The issue is that the definitions dict has its entries updated in two different places:

  1. definitions.update(m_definitions)
  2. definitions[model_name] = m_schema

When a model definition is written in line (2), the proper formfeed escape of the docstring is persisted. But if the same model is subsequently written by line (1), the previous docstring fix is clobbered over and we see the entire docstring.

This is a non-deterministic error, since flat_models is an unordered set. Depending on order of models in iteration, you could end up with either (1) or (2) as the final edit to a given model.

Solution

This fix works around the bug by handling the formfeed escape at the end of model processing. By that point our definitions dict will be final, so any edits to the models are guaranteed to make it into the return value.

Related issues

Questions & Alternatives

@github-actions

This comment was marked as outdated.

@MaxwellPayne MaxwellPayne force-pushed the bugfix/model-description-formfeed-inconsistency branch from 8a90482 to 5d3b953 Compare February 23, 2023 19:08
@github-actions

This comment was marked as outdated.

@MaxwellPayne MaxwellPayne force-pushed the bugfix/model-description-formfeed-inconsistency branch from 0eef5f7 to a4ea039 Compare February 23, 2023 19:18
@github-actions
Copy link
Contributor

📝 Docs preview for commit 3a15194 at: https://63f7bcb8cac94d008f451d7c--fastapi.netlify.app

@tiangolo tiangolo added the feature New feature or request label Oct 2, 2023
Copy link
Member

@YuriiMotov YuriiMotov left a comment

Choose a reason for hiding this comment

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

Preconditions:

  • Pydantic V1
  • Nested model has description with formfeed character

Symptoms:
Generated openapi schema will be inconsistent (sometimes the part after \f will be truncated, sometimes not).
test_openapi_schema (from test_get_model_definitions_formfeed_escape.py) will fail sometimes.
test_model_description_escaped_with_formfeed will always fail for sort_reversed=False.

Tests in test_get_model_definitions_formfeed_escape.py prove that the result of _compat.get_model_definitions depends on the order of models passed in flat_models parameter.

Explanation:

  • Address is nested into Facility.
  • When Address model goes before Facility in flat_models (input parameter of _compat.get_model_definitions), it's added to definitions and its description is updated (truncated by formfeed charecter).
  • But on the second iteration of for-loop, we have Address in m_definitions (since Address is nested into Facility), and definitions.update(m_definitions) overrides existing schema of Address in definitions.
  • So, definitions now contain the Address schema with original description (not truncated).

The idea of the fix is to add all models to definitions first and only then update description's of all models.

Looks good to me.

@YuriiMotov YuriiMotov changed the title Fix inconsistent processing of model docstring formfeed char 🐛 Fix inconsistent processing of model docstring formfeed char with Pydantic V1 Jul 18, 2025
@YuriiMotov YuriiMotov added bug Something isn't working and removed feature New feature or request labels Jul 18, 2025
Copy link
Member

@tiangolo tiangolo left a comment

Choose a reason for hiding this comment

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

Nice, sounds good, thank you! 🚀

Comment on lines +141 to +156
class SortedTypeSet(set):
"""
Set of Types whose `__iter__()` method yields results sorted by the type names
"""

def __init__(self, seq: Set[Type[Any]], *, sort_reversed: bool):
super().__init__(seq)
self.sort_reversed = sort_reversed

def __iter__(self) -> Iterator[Type[Any]]:
members_sorted = sorted(
super().__iter__(),
key=lambda type_: type_.__name__,
reverse=self.sort_reversed,
)
yield from members_sorted
Copy link
Member

Choose a reason for hiding this comment

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

Very fancy! ✨ ✔️

@tiangolo tiangolo merged commit 86e5157 into fastapi:master Sep 20, 2025
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants