diff --git a/assets/api/writer-generator/python/fhirpy_base_model.py b/assets/api/writer-generator/python/fhirpy_base_model.py index ace66c95..7226c605 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 4c41c0bd..862422df 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/examples/python-r4-us-core/fhir_types/fhirpy_base_model.py b/examples/python-r4-us-core/fhir_types/fhirpy_base_model.py index 4c41c0bd..862422df 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 4dc0e5f9..c93c8bce 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 3fd02b76..25d887cd 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' diff --git a/src/api/writer-generator/python/writer.ts b/src/api/writer-generator/python/writer.ts index c064b106..bb8e4a9c 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) { 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 5567bc62..a0e3ef88 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 70dfabf8..7ff45b55 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'