django-pydantic-field icon indicating copy to clipboard operation
django-pydantic-field copied to clipboard

Polymorphism (oneOf) with pydantic union discriminator

Open Zerotask opened this issue 1 year ago • 2 comments

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/

Zerotask avatar Sep 22 '23 12:09 Zerotask

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

henribru avatar Mar 15 '24 08:03 henribru

@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 avatar Mar 26 '24 10:03 surenkov