From f7343f083d88711e638c68f60416eb19ebed0548 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 26 Jun 2026 11:19:43 +0200 Subject: [PATCH 1/3] fix: PY: avoid resourceType shadow warnings on family base types Concrete resources subclass the abstract family types (Resource, DomainResource). The fhirpy base model registers resourceType as a class attribute from the field default, which the subclasses then shadow, producing Pydantic UserWarnings on import. Omit the default on family/abstract types (their resourceType becomes a required str validated by pattern) so no class attribute is registered, and guard the fhirpy registration against PydanticUndefined. --- .../api/writer-generator/python/fhirpy_base_model.py | 6 +++++- .../python/fhirpy_base_model_camel_case.py | 6 +++++- src/api/writer-generator/python/writer.ts | 10 +++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/assets/api/writer-generator/python/fhirpy_base_model.py b/assets/api/writer-generator/python/fhirpy_base_model.py index ace66c95c..7226c605e 100644 --- a/assets/api/writer-generator/python/fhirpy_base_model.py +++ b/assets/api/writer-generator/python/fhirpy_base_model.py @@ -1,5 +1,6 @@ from typing import Any, Union, Optional, Iterator, Tuple, Dict from pydantic import BaseModel, Field +from pydantic_core import PydanticUndefined from typing import Protocol @@ -22,7 +23,10 @@ class FhirpyBaseModel(BaseModel): def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: super().__pydantic_init_subclass__(**kwargs) field = cls.model_fields.get("resource_type") or cls.model_fields.get("resourceType") - if field is not None and field.default is not None: + # Only concrete resources carry a default resourceType. Abstract/family base types + # (Resource, DomainResource) leave it unset, so we skip them to avoid registering a + # class attribute that concrete subclasses would shadow. + if field is not None and field.default is not None and field.default is not PydanticUndefined: type.__setattr__(cls, "resourceType", str(field.default)) def __iter__(self) -> Iterator[Tuple[str, Any]]: # type: ignore[override] diff --git a/assets/api/writer-generator/python/fhirpy_base_model_camel_case.py b/assets/api/writer-generator/python/fhirpy_base_model_camel_case.py index 4c41c0bdc..862422dfe 100644 --- a/assets/api/writer-generator/python/fhirpy_base_model_camel_case.py +++ b/assets/api/writer-generator/python/fhirpy_base_model_camel_case.py @@ -1,5 +1,6 @@ from typing import Any, Union, Optional, Iterator, Tuple, Dict from pydantic import BaseModel, Field +from pydantic_core import PydanticUndefined from typing import Protocol @@ -21,7 +22,10 @@ class FhirpyBaseModel(BaseModel): def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: super().__pydantic_init_subclass__(**kwargs) field = cls.model_fields.get("resource_type") or cls.model_fields.get("resourceType") - if field is not None and field.default is not None: + # Only concrete resources carry a default resourceType. Abstract/family base types + # (Resource, DomainResource) leave it unset, so we skip them to avoid registering a + # class attribute that concrete subclasses would shadow. + if field is not None and field.default is not None and field.default is not PydanticUndefined: type.__setattr__(cls, "resourceType", str(field.default)) def __iter__(self) -> Iterator[Tuple[str, Any]]: # type: ignore[override] diff --git a/src/api/writer-generator/python/writer.ts b/src/api/writer-generator/python/writer.ts index c064b106c..bb8e4a9cf 100644 --- a/src/api/writer-generator/python/writer.ts +++ b/src/api/writer-generator/python/writer.ts @@ -468,11 +468,15 @@ export class Python extends Writer { } private generateResourceTypeField(schema: SpecializationTypeSchema): void { - // Always type as `str`; the value is validated on the pydantic side via `pattern`. - // A `Literal[...]` here would shadow the parent's field and trigger Pydantic warnings. + // Type as `str`; the value is validated on the pydantic side via `pattern`. + const hasChildren = (schema.typeFamily?.resources?.length ?? 0) > 0; this.line(`${this.nameFormatFunction("resourceType")}: str = Field(`); this.indentBlock(() => { - this.line(`default='${schema.identifier.name}',`); + // Family/abstract base types (Resource, DomainResource) are subclassed by concrete + // resources and never instantiated directly. Omitting the default keeps `resourceType` + // out of the class namespace (see fhirpy_base_model.__pydantic_init_subclass__), so the + // concrete subclasses don't "shadow" it and trigger Pydantic UserWarnings. + if (!hasChildren) this.line(`default='${schema.identifier.name}',`); this.line(`alias='resourceType',`); this.line(`serialization_alias='resourceType',`); if (!this.forFhirpyClient) { From fabb6b0e2034abd18e46324c863fd2a3c82fa0c5 Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 26 Jun 2026 11:19:43 +0200 Subject: [PATCH 2/3] fix: PY: update snapshots for family-type resourceType --- .../multi-package/__snapshots__/local-package.test.ts.snap | 1 - .../multi-package/__snapshots__/sql-on-fhir.test.ts.snap | 1 - 2 files changed, 2 deletions(-) diff --git a/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap b/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap index 5567bc62d..a0e3ef882 100644 --- a/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap +++ b/test/api/write-generator/multi-package/__snapshots__/local-package.test.ts.snap @@ -256,7 +256,6 @@ T = TypeVar('T', bound=Resource, default=Resource) class DomainResource(Resource, Generic[T]): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") resourceType: str = Field( - default='DomainResource', alias='resourceType', serialization_alias='resourceType', pattern='DomainResource' diff --git a/test/api/write-generator/multi-package/__snapshots__/sql-on-fhir.test.ts.snap b/test/api/write-generator/multi-package/__snapshots__/sql-on-fhir.test.ts.snap index 70dfabf8e..7ff45b550 100644 --- a/test/api/write-generator/multi-package/__snapshots__/sql-on-fhir.test.ts.snap +++ b/test/api/write-generator/multi-package/__snapshots__/sql-on-fhir.test.ts.snap @@ -196,7 +196,6 @@ T = TypeVar('T', bound=Resource, default=Resource) class DomainResource(Resource, Generic[T]): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") resourceType: str = Field( - default='DomainResource', alias='resourceType', serialization_alias='resourceType', pattern='DomainResource' From d78781867886aec9b6215b469185a61ee318d10d Mon Sep 17 00:00:00 2001 From: Aleksandr Penskoi Date: Fri, 26 Jun 2026 11:19:43 +0200 Subject: [PATCH 3/3] fix: PY: regenerate python-r4-us-core example for family-type resourceType --- examples/python-r4-us-core/fhir_types/fhirpy_base_model.py | 6 +++++- .../fhir_types/hl7_fhir_r4_core/domain_resource.py | 1 - .../fhir_types/hl7_fhir_r4_core/resource.py | 1 - 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/python-r4-us-core/fhir_types/fhirpy_base_model.py b/examples/python-r4-us-core/fhir_types/fhirpy_base_model.py index 4c41c0bdc..862422dfe 100644 --- a/examples/python-r4-us-core/fhir_types/fhirpy_base_model.py +++ b/examples/python-r4-us-core/fhir_types/fhirpy_base_model.py @@ -1,5 +1,6 @@ from typing import Any, Union, Optional, Iterator, Tuple, Dict from pydantic import BaseModel, Field +from pydantic_core import PydanticUndefined from typing import Protocol @@ -21,7 +22,10 @@ class FhirpyBaseModel(BaseModel): def __pydantic_init_subclass__(cls, **kwargs: Any) -> None: super().__pydantic_init_subclass__(**kwargs) field = cls.model_fields.get("resource_type") or cls.model_fields.get("resourceType") - if field is not None and field.default is not None: + # Only concrete resources carry a default resourceType. Abstract/family base types + # (Resource, DomainResource) leave it unset, so we skip them to avoid registering a + # class attribute that concrete subclasses would shadow. + if field is not None and field.default is not None and field.default is not PydanticUndefined: type.__setattr__(cls, "resourceType", str(field.default)) def __iter__(self) -> Iterator[Tuple[str, Any]]: # type: ignore[override] diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/domain_resource.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/domain_resource.py index 4dc0e5f9a..c93c8bce5 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/domain_resource.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/domain_resource.py @@ -18,7 +18,6 @@ class DomainResource(Resource, Generic[T]): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") resourceType: str = Field( - default='DomainResource', alias='resourceType', serialization_alias='resourceType', pattern='DomainResource' diff --git a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/resource.py b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/resource.py index 3fd02b76e..25d887cde 100644 --- a/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/resource.py +++ b/examples/python-r4-us-core/fhir_types/hl7_fhir_r4_core/resource.py @@ -15,7 +15,6 @@ class Resource(FhirpyBaseModel): model_config = ConfigDict(validate_by_name=True, serialize_by_alias=True, extra="forbid") resourceType: str = Field( - default='Resource', alias='resourceType', serialization_alias='resourceType', pattern='Resource'