Skip to content

Commit

Permalink
Fix Pydantic inherited model failing (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
faph authored Oct 3, 2023
2 parents 737420e + 0c8b3ba commit bae7cfb
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 1 deletion.
36 changes: 36 additions & 0 deletions docs/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ To *disable* this, pass the option :attr:`py_avro_schema.Option.NO_DOC`.

Recursive or repeated reference to the same Pydantic class is supported. After the first time the schema is output, any subsequent references are by name only.

.. warning::
When using a hierarchy of Pydantic model classes, recursive type references are supported in the *final class only* and not in any inherited/base class.


Plain Python classes
~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -193,6 +196,39 @@ Union members can be any other type supported by **py-avro-schema**.
When defined as a class field with a **default** value, the union members may be re-ordered to ensure that the first member matches the type of the default value.


Forward references
~~~~~~~~~~~~~~~~~~

Avro schema: any named schema

**py-avro-schema** generally supports "forward" or recursive references, for example when a class attribute has the same
type as a the class itself.

Example::

@dataclasses.dataclass
class PyType:
field_a: "PyType"

Is output as:

.. code-block:: json
{
"type": "record",
"name": "PyType",
"fields": [
{
"name": "field_a",
"type": "PyType",
},
],
}
.. warning::
When using a hierarchy of **Pydantic** model classes, recursive type references are supported in the *final class only* and not in any inherited/base class.


Collections
-----------

Expand Down
4 changes: 3 additions & 1 deletion src/py_avro_schema/_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -835,7 +835,9 @@ def _record_field(self, name: str, py_field: pydantic.fields.FieldInfo) -> Recor
# Pydantic 2 resolves forward references for us. To avoid infinite recursion, we check if the unresolved raw
# annoation is a forward reference. If so, we use that instead of Pydantic's resolved type hint. There might be
# a better way to un-resolve the forward reference...
if isinstance(self.raw_annotations[name], (str, ForwardRef)):
# This does not support forward references from base model classes. If required we might need to traverse class
# hierarchy to get the raw annotations?
if isinstance(self.raw_annotations.get(name), (str, ForwardRef)):
py_type = self.raw_annotations[name]
else:
py_type = py_field.annotation
Expand Down
49 changes: 49 additions & 0 deletions tests/test_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,52 @@ class PyType(pydantic.BaseModel):
],
}
assert_schema(PyType, expected)


def test_model_inheritance():
class PyTypeCustomBase(pydantic.BaseModel):
field_a: str

class PyType(PyTypeCustomBase):
field_b: str

expected = {
"type": "record",
"name": "PyType",
"fields": [
{
"name": "field_a",
"type": "string",
},
{
"name": "field_b",
"type": "string",
},
],
}
assert_schema(PyType, expected)


@pytest.mark.xfail(reason="Forward references in base classes not supported yet")
def test_model_inheritance_self_ref_in_base():
class PyTypeCustomBase(pydantic.BaseModel):
field_a: "PyTypeCustomBase"

class PyType(PyTypeCustomBase):
field_b: str

expected = {
"type": "record",
"name": "PyType",
"fields": [
{
"name": "field_a",
"type": "PyTypeCustomBase",
},
{
"name": "field_b",
"type": "string",
},
],
}
assert_schema(PyType, expected)

0 comments on commit bae7cfb

Please sign in to comment.