Skip to content

Commit 493d02e

Browse files
authored
Add --use-decimal-for-multiple-of option to avoid floating-point precision issues (#2656)
* Add --use-decimal-for-multiple-of option to improve precision for float/number fields * Add test and output for --use-decimal-for-multiple-of option
1 parent b6151d5 commit 493d02e

19 files changed

Lines changed: 144 additions & 0 deletions

File tree

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,10 @@ Typing customization:
381381
times.
382382
--use-annotated Use typing.Annotated for Field(). Also, `--field-constraints` option
383383
will be enabled.
384+
--use-decimal-for-multiple-of
385+
Use condecimal instead of confloat for float/number fields with
386+
multipleOf constraint (Pydantic only). Avoids floating-point
387+
precision issues in validation.
384388
--use-enum-values-in-discriminator
385389
Use enum member literals in discriminator fields instead of string
386390
literals

docs/index.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,10 @@ Typing customization:
373373
times.
374374
--use-annotated Use typing.Annotated for Field(). Also, `--field-constraints` option
375375
will be enabled.
376+
--use-decimal-for-multiple-of
377+
Use condecimal instead of confloat for float/number fields with
378+
multipleOf constraint (Pydantic only). Avoids floating-point
379+
precision issues in validation.
376380
--use-enum-values-in-discriminator
377381
Use enum member literals in discriminator fields instead of string
378382
literals

src/datamodel_code_generator/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,7 @@ def generate( # noqa: PLR0912, PLR0913, PLR0914, PLR0915
420420
use_annotated: bool = False,
421421
use_serialize_as_any: bool = False,
422422
use_non_positive_negative_number_constrained_types: bool = False,
423+
use_decimal_for_multiple_of: bool = False,
423424
original_field_name_delimiter: str | None = None,
424425
use_double_quotes: bool = False,
425426
use_union_operator: bool = False,
@@ -661,6 +662,7 @@ def get_header_and_first_line(csv_file: IO[str]) -> dict[str, Any]:
661662
use_annotated=use_annotated,
662663
use_serialize_as_any=use_serialize_as_any,
663664
use_non_positive_negative_number_constrained_types=use_non_positive_negative_number_constrained_types,
665+
use_decimal_for_multiple_of=use_decimal_for_multiple_of,
664666
original_field_name_delimiter=original_field_name_delimiter,
665667
use_double_quotes=use_double_quotes,
666668
use_union_operator=use_union_operator,

src/datamodel_code_generator/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ def validate_all_exports_collision_strategy(cls, values: dict[str, Any]) -> dict
426426
use_annotated: bool = False
427427
use_serialize_as_any: bool = False
428428
use_non_positive_negative_number_constrained_types: bool = False
429+
use_decimal_for_multiple_of: bool = False
429430
original_field_name_delimiter: Optional[str] = None # noqa: UP045
430431
use_double_quotes: bool = False
431432
collapse_root_models: bool = False
@@ -871,6 +872,7 @@ def main(args: Sequence[str] | None = None) -> Exit: # noqa: PLR0911, PLR0912,
871872
use_annotated=config.use_annotated,
872873
use_serialize_as_any=config.use_serialize_as_any,
873874
use_non_positive_negative_number_constrained_types=config.use_non_positive_negative_number_constrained_types,
875+
use_decimal_for_multiple_of=config.use_decimal_for_multiple_of,
874876
original_field_name_delimiter=config.original_field_name_delimiter,
875877
use_double_quotes=config.use_double_quotes,
876878
collapse_root_models=config.collapse_root_models,

src/datamodel_code_generator/arguments.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,13 @@ def start_section(self, heading: str | None) -> None:
373373
action="store_true",
374374
default=None,
375375
)
376+
typing_options.add_argument(
377+
"--use-decimal-for-multiple-of",
378+
help="Use condecimal instead of confloat for float/number fields with multipleOf constraint "
379+
"(Pydantic only). Avoids floating-point precision issues in validation.",
380+
action="store_true",
381+
default=None,
382+
)
376383
typing_options.add_argument(
377384
"--use-one-literal-as-default",
378385
help="Use one literal as default value for one literal field",

src/datamodel_code_generator/model/dataclass.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ def __init__( # noqa: PLR0913, PLR0917
166166
use_generic_container_types: bool = False, # noqa: FBT001, FBT002
167167
strict_types: Sequence[StrictTypes] | None = None,
168168
use_non_positive_negative_number_constrained_types: bool = False, # noqa: FBT001, FBT002
169+
use_decimal_for_multiple_of: bool = False, # noqa: FBT001, FBT002
169170
use_union_operator: bool = False, # noqa: FBT001, FBT002
170171
use_pendulum: bool = False, # noqa: FBT001, FBT002
171172
target_datetime_class: DatetimeClassType = DatetimeClassType.Datetime,
@@ -179,6 +180,7 @@ def __init__( # noqa: PLR0913, PLR0917
179180
use_generic_container_types,
180181
strict_types,
181182
use_non_positive_negative_number_constrained_types,
183+
use_decimal_for_multiple_of,
182184
use_union_operator,
183185
use_pendulum,
184186
target_datetime_class,

src/datamodel_code_generator/model/msgspec.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ def __init__( # noqa: PLR0913, PLR0917
414414
use_generic_container_types: bool = False, # noqa: FBT001, FBT002
415415
strict_types: Sequence[StrictTypes] | None = None,
416416
use_non_positive_negative_number_constrained_types: bool = False, # noqa: FBT001, FBT002
417+
use_decimal_for_multiple_of: bool = False, # noqa: FBT001, FBT002
417418
use_union_operator: bool = False, # noqa: FBT001, FBT002
418419
use_pendulum: bool = False, # noqa: FBT001, FBT002
419420
target_datetime_class: DatetimeClassType | None = None,
@@ -427,6 +428,7 @@ def __init__( # noqa: PLR0913, PLR0917
427428
use_generic_container_types,
428429
strict_types,
429430
use_non_positive_negative_number_constrained_types,
431+
use_decimal_for_multiple_of,
430432
use_union_operator,
431433
use_pendulum,
432434
target_datetime_class,

src/datamodel_code_generator/model/pydantic/types.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ def __init__( # noqa: PLR0913, PLR0917
171171
use_generic_container_types: bool = False, # noqa: FBT001, FBT002
172172
strict_types: Sequence[StrictTypes] | None = None,
173173
use_non_positive_negative_number_constrained_types: bool = False, # noqa: FBT001, FBT002
174+
use_decimal_for_multiple_of: bool = False, # noqa: FBT001, FBT002
174175
use_union_operator: bool = False, # noqa: FBT001, FBT002
175176
use_pendulum: bool = False, # noqa: FBT001, FBT002
176177
target_datetime_class: DatetimeClassType | None = None,
@@ -184,6 +185,7 @@ def __init__( # noqa: PLR0913, PLR0917
184185
use_generic_container_types,
185186
strict_types,
186187
use_non_positive_negative_number_constrained_types,
188+
use_decimal_for_multiple_of,
187189
use_union_operator,
188190
use_pendulum,
189191
target_datetime_class,
@@ -268,6 +270,12 @@ def get_data_float_type( # noqa: PLR0911
268270
data_type_kwargs = self.transform_kwargs(kwargs, number_kwargs)
269271
strict = StrictTypes.float in self.strict_types
270272
if data_type_kwargs:
273+
# Use Decimal instead of float when multipleOf is present to avoid floating-point precision issues
274+
if self.use_decimal_for_multiple_of and "multiple_of" in data_type_kwargs:
275+
return self.data_type.from_import(
276+
IMPORT_CONDECIMAL,
277+
kwargs={k: Decimal(str(v)) for k, v in data_type_kwargs.items()},
278+
)
271279
if not strict:
272280
if data_type_kwargs == {"gt": 0}:
273281
return self.data_type.from_import(IMPORT_POSITIVE_FLOAT)

src/datamodel_code_generator/model/pydantic_v2/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __init__( # noqa: PLR0913, PLR0917
7070
use_generic_container_types: bool = False, # noqa: FBT001, FBT002
7171
strict_types: Sequence[StrictTypes] | None = None,
7272
use_non_positive_negative_number_constrained_types: bool = False, # noqa: FBT001, FBT002
73+
use_decimal_for_multiple_of: bool = False, # noqa: FBT001, FBT002
7374
use_union_operator: bool = False, # noqa: FBT001, FBT002
7475
use_pendulum: bool = False, # noqa: FBT001, FBT002
7576
target_datetime_class: DatetimeClassType | None = None,
@@ -83,6 +84,7 @@ def __init__( # noqa: PLR0913, PLR0917
8384
use_generic_container_types=use_generic_container_types,
8485
strict_types=strict_types,
8586
use_non_positive_negative_number_constrained_types=use_non_positive_negative_number_constrained_types,
87+
use_decimal_for_multiple_of=use_decimal_for_multiple_of,
8688
use_union_operator=use_union_operator,
8789
use_pendulum=use_pendulum,
8890
target_datetime_class=target_datetime_class,

src/datamodel_code_generator/model/types.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def __init__( # noqa: PLR0913, PLR0917
7373
use_generic_container_types: bool = False, # noqa: FBT001, FBT002
7474
strict_types: Sequence[StrictTypes] | None = None,
7575
use_non_positive_negative_number_constrained_types: bool = False, # noqa: FBT001, FBT002
76+
use_decimal_for_multiple_of: bool = False, # noqa: FBT001, FBT002
7677
use_union_operator: bool = False, # noqa: FBT001, FBT002
7778
use_pendulum: bool = False, # noqa: FBT001, FBT002
7879
target_datetime_class: DatetimeClassType | None = None,
@@ -86,6 +87,7 @@ def __init__( # noqa: PLR0913, PLR0917
8687
use_generic_container_types,
8788
strict_types,
8889
use_non_positive_negative_number_constrained_types,
90+
use_decimal_for_multiple_of,
8991
use_union_operator,
9092
use_pendulum,
9193
target_datetime_class,

0 commit comments

Comments
 (0)