When a FHIR resource contains a Decimal value with no fractional part (e.g., Decimal('19')), both model_dump() and model_dump_json() serialize it as 19.0. This is problematic because FHIR assigns semantic meaning to decimal precision: 19, 19.0, and 19.00 are clinically distinct values.
Reproduction:
from decimal import Decimal
from fhir.resources.observation import Observation
observation_dict = {
"resourceType": "Observation",
"status": "final",
"code": {"coding": [{"system": "http://loinc.org", "code": "12345-6"}]},
"component": [
{
"code": {"coding": [{"system": "http://loinc.org", "code": "12345-6"}]},
"valueQuantity": {
"value": Decimal("19"),
"unit": "mg/dL",
"system": "http://unitsofmeasure.org",
"code": "mg/dL",
},
}
],
}
observation = Observation(**observation_dict)
# Model stores the value correctly
print(repr(observation.component[0].valueQuantity.value))
# Output: Decimal('19')
# But both serialization methods convert to float
print(observation.model_dump_json())
# Output: ..."valueQuantity":{"value":19.0, ...
data = observation.model_dump()
print(type(data["component"][0]["valueQuantity"]["value"]))
# Output: <class 'float'>
Expected behavior:
Decimal('19') should serialize as 19 in JSON output, Decimal('19.0') as 19.0, etc. The original precision should be preserved.
Actual behavior:
All whole-number Decimal values are converted to float, resulting in a trailing .0. Additionally, model_dump() returns float instead of Decimal, making it impossible to recover the original precision downstream.
Impact:
- FHIR precision semantics are lost. Per the FHIR spec, the number of significant digits conveys measurement precision.
- Downstream FHIR consumers may interpret
19.0 differently from 19.
- There is no clean workaround using
model_dump() / model_dump_json() options (including round_trip=True and mode="json").
Suggested fix:
Add a custom field_serializer for DecimalType fields that preserves the original Decimal representation, e.g.:
from pydantic import field_serializer
from decimal import Decimal
@field_serializer('value', mode='plain')
@classmethod
def serialize_decimal(cls, v):
if isinstance(v, Decimal):
return int(v) if v == v.to_integral_value() else float(v)
return v
Or ideally, a serializer that fully preserves FHIR precision semantics by writing the Decimal as a JSON number with the exact original significant digits.
Environment:
- Python version: 3.12.7
- pydantic version: 2.12.4
- fhir.resources version: 8.1.0
When a FHIR resource contains a
Decimalvalue with no fractional part (e.g.,Decimal('19')), bothmodel_dump()andmodel_dump_json()serialize it as19.0. This is problematic because FHIR assigns semantic meaning to decimal precision:19,19.0, and19.00are clinically distinct values.Reproduction:
Expected behavior:
Decimal('19')should serialize as19in JSON output,Decimal('19.0')as19.0, etc. The original precision should be preserved.Actual behavior:
All whole-number
Decimalvalues are converted tofloat, resulting in a trailing.0. Additionally,model_dump()returnsfloatinstead ofDecimal, making it impossible to recover the original precision downstream.Impact:
19.0differently from19.model_dump()/model_dump_json()options (includinground_trip=Trueandmode="json").Suggested fix:
Add a custom
field_serializerforDecimalTypefields that preserves the originalDecimalrepresentation, e.g.:Or ideally, a serializer that fully preserves FHIR precision semantics by writing the
Decimalas a JSON number with the exact original significant digits.Environment: