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

Polymorphism (oneOf) with pydantic union discriminator #31

Open
Zerotask opened this issue Sep 22, 2023 · 2 comments
Open

Polymorphism (oneOf) with pydantic union discriminator #31

Zerotask opened this issue Sep 22, 2023 · 2 comments

Comments

@Zerotask
Copy link

I have the following pydantic models:

class EUTaxonomyValue(BaseModel):
    type: Literal["eu_taxonomy"] = "eu_taxonomy"
    nace_code: str
    economic_activity: str
    classification: str | None


class EconomicActivity(BaseModel):
    type: Literal["economic_activity"] = "economic_activity"
    activity_code: str
    activity_name: str


class DateRange(BaseModel):
    type: Literal["daterange"] = "daterange"
    start_date: date
    end_date: date

ComplexValue = Annotated[
    None | EUTaxonomyValue | EconomicActivity | DateRange,
    Field(discriminator="type", title="Complex Value"),
]

Now I define a new field value_complex in one of my serializers:

class FieldValueSerializer(serializers.ModelSerializer):
    value_complex = SchemaField(schema=ComplexValue, required=False)

I'm not sure if this issue is related to django-pydantic-field or drf-spectacular.

The generated Open API 3 Schema is invalid.

#/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0/properties/field_values/items/properties/value_complex must NOT have additional properties #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0/properties/field_values/items/properties/value_complex must have required property '$ref' #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0/properties/field_values/items/properties/value_complex must match exactly one schema in oneOf #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0/properties/field_values/items must have required property '$ref' #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0/properties/field_values/items must match exactly one schema in oneOf #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0/properties/field_values must have required property '$ref' #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0/properties/field_values must match exactly one schema in oneOf #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0 must have required property '$ref' #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report/allOf/0 must match exactly one schema in oneOf #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report must have required property '$ref' #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema/properties/report must match exactly one schema in oneOf #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema must have required property '$ref' #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200/content/application~1json/schema must match exactly one schema in oneOf #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200 must have required property '$ref' #/paths/~1api~1report-requests~1{id}~1details~1/get/responses/200 must match exactly one schema in oneOf #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema/properties/results/items/properties/value_complex must NOT have additional properties #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema/properties/results/items/properties/value_complex must have required property '$ref' #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema/properties/results/items/properties/value_complex must match exactly one schema in oneOf #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema/properties/results/items must have required property '$ref' #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema/properties/results/items must match exactly one schema in oneOf #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema/properties/results must have required property '$ref' #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema/properties/results must match exactly one schema in oneOf #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema must have required property '$ref' #/paths/~1api~1report~1field-values~1/get/responses/200/content/application~1json/schema must match exactly one schema in oneOf #/paths/~1api~1report~1field-values~1/get/responses/200 must have required property '$ref' #/paths/~1api~1report~1field-values~1/get/responses/200 must match exactly one schema in oneOf
value_complex:
          title: value_complex
          discriminator:
            propertyName: type
            mapping:
              eu_taxonomy: '#/definitions/EUTaxonomyValue'
              economic_activity: '#/definitions/EconomicActivity'
              daterange: '#/definitions/DateRange'
          oneOf:
          - title: EUTaxonomyValue
            type: object
            properties:
              type:
                title: Type
                default: eu_taxonomy
                enum:
                - eu_taxonomy
                type: string
              nace_code:
                title: Nace Code
                type: string
              economic_activity:
                title: Economic Activity
                type: string
              classification:
                title: Classification
                type: string
            required:
            - nace_code
            - economic_activity
          - title: EconomicActivity
            type: object
            properties:
              type:
                title: Type
                default: economic_activity
                enum:
                - economic_activity
                type: string
              activity_code:
                title: Activity Code
                type: string
              activity_name:
                title: Activity Name
                type: string
            required:
            - activity_code
            - activity_name
          - title: DateRange
            type: object
            properties:
              type:
                title: Type
                default: daterange
                enum:
                - daterange
                type: string
              start_date:
                title: Start Date
                type: string
                format: date
              end_date:
                title: End Date
                type: string
                format: date
            required:
            - start_date
            - end_date
          definitions:
            EUTaxonomyValue:
              title: EUTaxonomyValue
              type: object
              properties:
                type:
                  title: Type
                  default: eu_taxonomy
                  enum:
                  - eu_taxonomy
                  type: string
                nace_code:
                  title: Nace Code
                  type: string
                economic_activity:
                  title: Economic Activity
                  type: string
                classification:
                  title: Classification
                  type: string
              required:
              - nace_code
              - economic_activity
            EconomicActivity:
              title: EconomicActivity
              type: object
              properties:
                type:
                  title: Type
                  default: economic_activity
                  enum:
                  - economic_activity
                  type: string
                activity_code:
                  title: Activity Code
                  type: string
                activity_name:
                  title: Activity Name
                  type: string
              required:
              - activity_code
              - activity_name
            DateRange:
              title: DateRange
              type: object
              properties:
                type:
                  title: Type
                  default: daterange
                  enum:
                  - daterange
                  type: string
                start_date:
                  title: Start Date
                  type: string
                  format: date
                end_date:
                  title: End Date
                  type: string
                  format: date
              required:
              - start_date
              - end_date

Regarding https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/ oneOf should have a list of $ref and those 3 pydantic models should be defined in #/components/schemas/

@henribru
Copy link

In addition to this issue with serializers this use case doesn't seem to work properly in models either. For me, on v0.3.3, I'm not able to create a migration if I use a Annotated[A| B, Field(discriminator="type")] as the schema for a model field. It leads to:

Traceback (most recent call last):
  File "/.../backend/manage.py", line 25, in <module>
    main()
  File "/.../backend/manage.py", line 21, in main
    execute_from_command_line(sys.argv)
  File "/.../lib/python3.11/site-packages/django/core/management/__init__.py", line 442, in execute_from_command_line
    utility.execute()
  File "/.../lib/python3.11/site-packages/django/core/management/__init__.py", line 436, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/.../lib/python3.11/site-packages/django/core/management/base.py", line 412, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/.../lib/python3.11/site-packages/django/core/management/base.py", line 458, in execute
    output = self.handle(*args, **options)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/core/management/base.py", line 106, in wrapper
    res = handle_func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/core/management/commands/makemigrations.py", line 259, in handle
    self.write_migration_files(changes)
  File "/.../lib/python3.11/site-packages/django/core/management/commands/makemigrations.py", line 364, in write_migration_files
    migration_string = writer.as_string()
                       ^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/db/migrations/writer.py", line 141, in as_string
    operation_string, operation_imports = OperationWriter(operation).serialize()
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/db/migrations/writer.py", line 99, in serialize
    _write(arg_name, arg_value)
  File "/.../lib/python3.11/site-packages/django/db/migrations/writer.py", line 51, in _write
    arg_string, arg_imports = MigrationWriter.serialize(item)
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/db/migrations/writer.py", line 282, in serialize
    return serializer_factory(value).serialize()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/db/migrations/serializer.py", line 42, in serialize
    item_string, item_imports = serializer_factory(item).serialize()
                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/db/migrations/serializer.py", line 226, in serialize
    return self.serialize_deconstructed(path, args, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/db/migrations/serializer.py", line 91, in serialize_deconstructed
    arg_string, arg_imports = serializer_factory(arg).serialize()
                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django_pydantic_field/compat/django.py", line 81, in serialize
    arg_repr, arg_imports = serializer_factory(arg).serialize()
                            ^^^^^^^^^^^^^^^^^^^^^^^
  File "/.../lib/python3.11/site-packages/django/db/migrations/serializer.py", line 391, in serializer_factory
    raise ValueError(
ValueError: Cannot serialize: FieldInfo(annotation=NoneType, required=True, discriminator='type')
There are some values Django cannot serialize into migration files.
For more, see https://docs.djangoproject.com/en/4.2/topics/migrations/#migration-serializing

@surenkov
Copy link
Owner

@henribru I have played around a bit with Annotated schemas in #52 and figured out the way to serialize them as long as annotated metadata contains only serializable entities, e.g. dataclasses (for annotated_types package) and Pydantic's FieldInfo.

As a workaround for the time being I can suggest using a pydantic.RootModel wrapper around your annotated expression, if that suits your particular case.

surenkov added a commit that referenced this issue Mar 30, 2024
This PR is related to #31

This PR adds the support of typing.Annotated[...] expressions, both through schema= attribute or field annotation syntax; though I find the schema=typing.Annotated[...] variant highly discouraged.

The current limitation is not in the field itself, but in possible Annotated metadata -- practically it can contain anything, and Django migrations serializers could refuse to write it to migrations.

For most relevant types in context of Pydantic, I wrote the specific serializers (particularly for pydantic.FieldInfo, pydantic.Representation and raw dataclasses), thus it should cover the majority of Annotated use cases
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants