Skip to content
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

Literal annotation does not render type to json-schema #8905

Closed
1 task done
bruno-f-cruz opened this issue Feb 27, 2024 · 12 comments · Fixed by #8944 or #9135
Closed
1 task done

Literal annotation does not render type to json-schema #8905

bruno-f-cruz opened this issue Feb 27, 2024 · 12 comments · Fixed by #8944 or #9135
Labels
bug V2 Bug related to Pydantic V2 pending Awaiting a response / confirmation

Comments

@bruno-f-cruz
Copy link
Contributor

Initial Checks

  • I confirm that I'm using Pydantic V2

Description

When defining a property as Literal, eg:

from pydantic import BaseModel
from typing import Literal


class Foo(BaseModel):
    bar: Literal["Bar"] = 'Bar'
    baz: Literal[None] = None

The output json-schema model loses the typing of the variable used for the literal. I.e.:

{
  "properties": {
    "bar": {
      "const": "Bar",
      "default": "Bar",
      "title": "Bar"
    },
    "baz": {
      "const": null,
      "default": null,
      "title": "Baz"
    }
  },
  "title": "Foo",
  "type": "object"
}

It would be nice to infer this type and correctly add it to the schema. This could be achieved by simply extending the literal_schema method

def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue:

with:

def literal_schema(self, schema: core_schema.LiteralSchema) -> JsonSchemaValue:
        """Generates a JSON schema that matches a literal value.

        Args:
            schema: The core schema.

        Returns:
            The generated JSON schema.
        """
        expected = [v.value if isinstance(v, Enum) else v for v in schema["expected"]]
        # jsonify the expected values
        expected = [to_jsonable_python(v) for v in expected]

        types = {type(e) for e in expected}

        if len(expected) == 1:
            if isinstance(expected[0], str):
                return {"const": expected[0], "type": "string"}
            elif isinstance(expected[0], int):
                return {"const": expected[0], "type": "integer"}
            elif isinstance(expected[0], float):
                return {"const": expected[0], "type": "number"}
            elif isinstance(expected[0], bool):
                return {"const": expected[0], "type": "boolean"}
            elif isinstance(expected[0], list):
                return {"const": expected[0], "type": "array"}
            elif expected[0] is None:
                return {"const": expected[0], "type": "null"}
            else:
                return {"const": expected[0]}

        if types == {str}:
            return {"enum": expected, "type": "string"}
        elif types == {int}:
            return {"enum": expected, "type": "integer"}
        elif types == {float}:
            return {"enum": expected, "type": "number"}
        elif types == {bool}:
            return {"enum": expected, "type": "boolean"}
        elif types == {list}:
            return {"enum": expected, "type": "array"}
        # there is not None case because if it's mixed it hits the final `else`
        # if it's a single Literal[None] then it becomes a `const` schema above
        else:
            return {"enum": expected}

Example Code

No response

Python, Pydantic & OS Version

pydantic version: 2.5.3
        pydantic-core version: 2.14.6
          pydantic-core build: profile=release pgo=true
                 install path: <...>\.venv\Lib\site-packages\pydantic
               python version: 3.11.7 (tags/v3.11.7:fa7a6f2, Dec  4 2023, 19:24:49) [MSC v.1937 64 bit (AMD64)]     
                     platform: Windows-10-10.0.19045-SP0
             related packages: typing_extensions-4.9.0
@bruno-f-cruz bruno-f-cruz added bug V2 Bug related to Pydantic V2 pending Awaiting a response / confirmation labels Feb 27, 2024
@WillDaSilva
Copy link

Related to this, it would be nice if StringConstraints had a values parameter (or similar) that would let you specify the literal allowed values. This would fulfill the same use-case as using the Literal type hint, but would allow for a dynamic list of strings to be used instead. Currently this behaviour can be approximated by using StringConstraints with pattern='^(literal_value_1|literal_value_2|etc)$', but the JSON schema this produces isn't as clear as it could be.

As a workaround, I'm using a dynamically generated string enum.

@dmontagu
Copy link
Contributor

dmontagu commented Mar 4, 2024

@WillDaSilva does the following not work for you?

from typing import Literal

from pydantic import BaseModel


class A(BaseModel):
    x: Literal['a', 'b', 'c']

print(A.model_json_schema())
#> {'properties': {'x': {'enum': ['a', 'b', 'c'], 'title': 'X', 'type': 'string'}}, 'required': ['x'], 'title': 'A', 'type': 'object'}

More generally though, I think we should do a better job of handling single-item literals as I have discovered some JSON-schema-consuming tools that misbehave if you use just a plain const instead of enum with a single item.

Hoping to have some improvements here shortly.

@WillDaSilva
Copy link

@dmontagu That works for when the list of possible values is static (i.e. hard-coded), but when you have the list of strings as a variable it does not work.

@dmontagu
Copy link
Contributor

dmontagu commented Mar 4, 2024

Ah, now I understand. Not sure how the other maintainers feel but I wouldn't be opposed to adding that option to StringConstraints.

@WillDaSilva I'll just note though in case it is useful that I think it is still technically possible to using Literal in a dynamic way to achieve this:

from typing import Literal, TYPE_CHECKING

from pydantic import BaseModel

choices = ['a', 'b', 'c']
if TYPE_CHECKING:
    # make type-checkers just see `str` so that they don't complain about the dynamic literal
    MyStringChoices = str
else:
    MyStringChoices = Literal[*choices]


class MyModel(BaseModel):
    x: MyStringChoices


print(MyModel.model_json_schema())
#> {'properties': {'x': {'enum': ['a', 'b', 'c'], 'title': 'X', 'type': 'string'}}, 'required': ['x'], 'title': 'MyModel', 'type': 'object'}

but I would agree that having StringConstraints support literal choices would be better than this.

@dmontagu
Copy link
Contributor

dmontagu commented Mar 4, 2024

@bruno-f-cruz does the change proposed in #8944 address your issue, or do you otherwise have any problems with it? It goes a bit farther than just adding the type in order to also address other issues at the same time, but if you (or anyone) have a problem with that change then it might be more reasonable to scale it back.

@bruno-f-cruz
Copy link
Contributor Author

I don't have an informed opinion on the full scope of your PR. That being said, it would solve this issue. Thanks!

@bruno-f-cruz
Copy link
Contributor Author

@dmontagu Sorry if i am too late to the party but I failed to noticed something in the previous test. While the string type is indeed added to the schema, the Literal[None] does not give rise to a typing in the schema. Can you think of a reason why it would be a bad idea to add this typing to the PR too? Thanks!

@bruno-f-cruz
Copy link
Contributor Author

@sydney-runkle Would it be possible to open this issue given my last comment or should I open a new one?

@sydney-runkle
Copy link
Member

@bruno-f-cruz,

Ah, thanks for the ping. Let's see what @dmontagu thinks!

@dmontagu
Copy link
Contributor

I think it was just an oversight. Would you like to open a PR adding better handling for None as an enum value?

@bruno-f-cruz
Copy link
Contributor Author

@dmontagu @sydney-runkle Check #9135. Would this be ok?

@sydney-runkle
Copy link
Member

@bruno-f-cruz,

Looks great, please add a test!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug V2 Bug related to Pydantic V2 pending Awaiting a response / confirmation
Projects
None yet
4 participants