Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Support numeric constraints for decimal.Decimal - #1006

Open
Siyet wants to merge 11 commits into
mainfrom
decimal-constraints-683
Open

Support numeric constraints for decimal.Decimal#1006
Siyet wants to merge 11 commits into
mainfrom
decimal-constraints-683

Conversation

@Siyet

@Siyet Siyet commented Apr 9, 2026

Copy link
Copy Markdown
Contributor

Reopening #998 (auto-closed when fork was deleted).

Fixes #683.

Previously, the numeric constraints gt, ge, lt, le, and multiple_of in msgspec.Meta were only valid for int and float types. This PR extends support to decimal.Decimal.

PR #692 by @cocorigon has been open since May 2024. The author has since explicitly declined to continue it and invited others to take it over. This is a fresh implementation with documentation and full test coverage.

Changes

  • _core.c: Added five new TypeNode constraint flags backed by PyObject* slots storing Decimal instances. A new ms_check_decimal_constraints() function is called after every Decimal decode path.
  • __init__.pyi: Updated Meta.__init__ parameter types to include Decimal.
  • docs/constraints.rst: Updated Numeric Constraints section to mention decimal.Decimal.
  • tests/unit/test_constraints.py: Added TestDecimalConstraints covering all constraint types.

Notable design decisions

  • multiple_of for Decimal uses Python's % operator, which gives exact arithmetic.
  • Constraint bounds passed as float are converted to Decimal via string representation to preserve precision.

@ofek

ofek commented Apr 9, 2026

Copy link
Copy Markdown
Member

It looks like the Git history needs to be fixed on this one.

@Siyet
Siyet requested review from jcrist and ofek April 10, 2026 07:32
@Siyet

Siyet commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

The failing build job here is unrelated to this PR: it's the link checker tripping on the Pydantic docs redirect (docs.pydantic.dev/latest/pydantic.dev/docs/validation/...), which is fixed in #1008. Once #1008 lands and this branch is rebased, CI should go green.

@Siyet

Siyet commented Apr 10, 2026

Copy link
Copy Markdown
Contributor Author

@ofek done — force-pushed a clean rewrite of the branch:

  • Squashed into a single commit with a proper message and Closes #683 reference.
  • Rebased onto current main, dropping all the leftover commits from the deleted fork (CODEOWNERS, package rename, doc-link rewrites, plus changes that belong to other PRs).
  • Scope is now strictly the four Decimal-relevant files: _core.c, __init__.pyi, docs/constraints.rst, tests/unit/test_constraints.py. Diff stat is +353/-52.
  • MS_CONSTR_DECIMAL_* flags claim bits 36-40, the lowest unused slots in TypeNode->types. If Support Literal[True] and Literal[False] types #1004 lands first, I'll rebase to whatever bits are next free.

Build job is still red on the link checker but that's the unrelated #1008 fix; nothing in this branch touches it.

Extends gt, ge, lt, le, and multiple_of Meta constraints to work with
decimal.Decimal types, enabling exact arithmetic comparisons and
sub-integer precision constraints without floating point issues.

Constraint bounds passed as int or float are coerced to Decimal via
their string representation to preserve precision. multiple_of uses
Python's % operator for exact arithmetic.

Closes #683
@Siyet
Siyet force-pushed the decimal-constraints-683 branch from d2ce46f to 023b9e1 Compare April 20, 2026 19:22
@Siyet

Siyet commented Apr 20, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main now that #1004 has landed. #1004 took bits 36-37 for MS_TYPE_BOOLLITERAL_TRUE/FALSE, so I shifted the Decimal constraint flags to the next free slots:

  • MS_CONSTR_DECIMAL_GT/GE/LT/LE -> bits 38-41 (the remaining gap between types and existing constraints)
  • MS_CONSTR_DECIMAL_MULTIPLE_OF -> bit 61 (the only free slot that's left below the reserved MS_EXTRA_FLAG at 63)

No other constraint bits were moved. SLOT numbering and the TypeNode->details[] ordering are unchanged (SLOTs use the named constants, not raw bit positions).

@provinzkraut provinzkraut left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should have a check that disallows Decimal for gt, ge, etc. constraints where the type is not also a Decimal

Coercing a Decimal bound to int or float silently drops precision, which
defeats the point of specifying the bound as a Decimal in the first
place. Only allow Decimal bounds when the annotated type is also
Decimal, and raise a TypeError otherwise at TypeNode construction time.
@Siyet

Siyet commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

@provinzkraut added the check: a Decimal in gt/ge/lt/le/multiple_of now raises TypeError at encoder construction time when the annotated type isn't Decimal. Coercing to int/float loses precision, so a Decimal bound there is pointless.

Covered with a parametrized test, docs updated.

Comment thread src/msgspec/__init__.pyi Outdated
lt: Union[int, float, None] = None,
le: Union[int, float, None] = None,
multiple_of: Union[int, float, None] = None,
gt: Union[int, float, Decimal, None] = None,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can now be a type alias, since it is repeated quite a lot and can be changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, pulled it out into _NumericBound = int | float | Decimal | None.

Comment thread src/msgspec/_core.c Outdated
return false;
}
}
else if (PyObject_IsInstance(val, mod->DecimalType)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows Decimal subclasses, but you never test them. Since int and float subclasses are not allowed above - what is the correct way to handle this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

int/float use *_CheckExact because bool is an int subclass (we don't want to silently accept True as a numeric bound). Decimal has no such footgun subclass, and _constr_as_decimal normalizes the bound through Decimal(obj) at TypeNode-build time anyway, so a subclass's overridden dunders never reach the checks (verified with a hostile subclass overriding __gt__/__mod__ to raise: they're never called). Accepting subclasses is intentional; added test_decimal_subclass_bound.

Comment thread src/msgspec/_core.c Outdated
if ((out = TypeNode_traverse(node, visit, arg)) != 0) return out;
}
/* Traverse Decimal constraint PyObject* */
if (self->types & (MS_CONSTR_DECIMAL_GT | MS_CONSTR_DECIMAL_GE)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done before return out. Otherwise it can be left in a broken state.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, moved the Decimal-constraint Py_VISITs before the loops that can return out early. They're direct references on this node, so visiting them before recursing into children is the right place. (The Decimal pointers aren't counted in n_obj, so there's no double-visit.)

Comment thread src/msgspec/_core.c
PyObject *c = TypeNode_get_constr_decimal_min(type);
int ok = PyObject_RichCompareBool(obj, c, Py_GT);
if (ok == -1) return false;
if (MS_UNLIKELY(!ok)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that it is unlikely. I would say that ok == -1 is unlikely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing ms_decode_constr_int (and the float/str checks) mark the validation-failure path !ok as MS_UNLIKELY since valid data is the hot path, so I kept that for consistency. You're right about the error path though: marked ok < 0 as MS_UNLIKELY (and switched == -1 to < 0) in all four comparison blocks.

Comment thread src/msgspec/_core.c Outdated
PyObject *modulo = PyNumber_Remainder(obj, c);
if (modulo == NULL) return false;
PyObject *zero = PyLong_FromLong(0);
if (zero == NULL) { Py_DECREF(modulo); return false; }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (zero == NULL) { Py_DECREF(modulo); return false; }
if (zero == NULL) {
Py_DECREF(modulo);
return false;
}

style nit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dropped this block entirely, there's no zero to allocate anymore (see the multiple_of comment below).

Comment thread src/msgspec/_core.c
PyObject *c = TypeNode_get_constr_decimal_multiple_of(type);
PyObject *modulo = PyNumber_Remainder(obj, c);
if (modulo == NULL) return false;
PyObject *zero = PyLong_FromLong(0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to do the second compare in Python, you can do this in C.

Convert modulo to PyNumber_AsSsize_t (or whatever) and compare it with == 0 :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyNumber_AsSsize_t won't work here: Decimal has no __index__, so it'd raise TypeError on any remainder (including integral ones). Switched to PyObject_IsTrue(modulo) instead, a Decimal is falsy iff it's zero (including -0, 0E+10). That drops the zero allocation and stays correct for fractional remainders like Decimal("0.5").

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we allow Decimal subclasses, we can't use PyObject_IsTrue, because __bool__ can be changed in subclasses :(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, switched to an explicit PyObject_RichCompareBool(modulo, zero, Py_EQ) against a cached small int - it expresses the intent directly and doesn't depend on __bool__ at all.

For the record, I checked reachability while fixing this: the scenario can't actually trigger today. Subclass values never reach the check (convert rejects them with Expected `decimal`, got `WeirdDecimal` , and decode paths construct plain Decimal), subclass bounds are normalized via Decimal(bound) at collection, and decimal arithmetic returns base-class instances anyway, so the remainder was always a plain Decimal. Still, the explicit comparison is clearly better here, and there's now a test pinning the behavior with a __bool__-overriding subclass bound (test_multiple_of_decimal_subclass_bound_bool_override).

Comment thread docs/constraints.rst Outdated

`decimal.Decimal` bounds (``gt``, ``ge``, ``lt``, ``le``, ``multiple_of``)
are only valid on `decimal.Decimal` types. Applying a `decimal.Decimal`
bound to an ``int`` or ``float`` type raises a `TypeError`, since coercing

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I have a question about int here. How does it lose precision?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, "lose precision" was imprecise for int. It's not truncation: the bound would be converted via PyLong_AsLongLongAndOverflow, which needs __index__, and Decimal has none, so any Decimal (Decimal("2") as well as Decimal("1.5")) is rejected by the int conversion, and a fractional one has no integer equivalent at all. For float the bound would be rounded to the nearest binary float. Reworded the note to drop the incorrect "truncate".

# Conflicts:
#	src/msgspec/__init__.pyi
#	tests/unit/test_constraints.py
@Siyet
Siyet temporarily deployed to docs-preview June 15, 2026 22:43 — with GitHub Actions Inactive
@Siyet

Siyet commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

@provinzkraut this is already handled in this PR: a Decimal bound on an int/float type is rejected with a TypeError (typenode_collect_constraints, via PyObject_IsInstance(..., DecimalType)), covered by test_invalid_decimal_bound_on_non_decimal for all five of gt/ge/lt/le/multiple_of. The check runs at TypeNode construction, so it's protocol-agnostic (msgpack behaves identically).

@Siyet
Siyet temporarily deployed to docs-preview June 15, 2026 23:31 — with GitHub Actions Inactive
@Siyet
Siyet temporarily deployed to docs-preview June 15, 2026 23:39 — with GitHub Actions Inactive
Siyet added 2 commits July 2, 2026 17:00
Frees up 4 bits in the types bitmask (main now uses bits 38-39 for
frozendict, leaving too few free bits for per-constraint decimal bits).
Also compare the multiple_of remainder against zero explicitly instead
of relying on __bool__.
MS_TYPE_FROZENDICT took bits 38-39, MS_CONSTR_DECIMAL moved to bit 40.
@Siyet
Siyet temporarily deployed to docs-preview July 2, 2026 13:06 — with GitHub Actions Inactive
@Siyet

Siyet commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on main and reworked the constraint storage, two commits:

  1. Storage redesign (38f7e7c): decimal constraints now live in a single details slot holding a 5-tuple (gt, ge, lt, le, multiple_of) (None for unset), gated by one MS_CONSTR_DECIMAL bit. This was forced by bit-space math: Add frozendict support for 3.15+ #1052 took bits 38-39 for frozendict, leaving only 4 free bits (40, 41, 61, 62) in the types mask while the old per-constraint layout needed 5. Sharing bits with the float constraints isn't an option since constrained members of different numeric types can coexist in one union (test_mix_float_and_int). The single-slot shape follows the literal-lookup precedent (one bit, one object holding the details). Validation is unchanged behavior-wise; the check is already a cold path gated by one bit test.
  2. Merge main (def7a7b): trivial after the redesign, MS_CONSTR_DECIMAL landed on bit 40.

Also addressed the PyObject_IsTrue review comment: the remainder is now compared against zero explicitly (see inline reply), with a regression test for a __bool__-overriding subclass.

Verified locally on 3.12 and 3.15.0a7 (WSL): full unit suite green on both (6351 / 6439 passed), decimal constraints + frozendict coexist correctly in one union on 3.15 (slot ordering with bits 38-39 + 40), and 100k decoder create/destroy cycles show zero RSS growth (constraint tuple lifecycle is leak-free).

@codspeed-hq

codspeed-hq Bot commented Jul 2, 2026

Copy link
Copy Markdown

Merging this PR will degrade performance by 10.27%

❌ 1 regressed benchmark
✅ 138 untouched benchmarks
⏩ 135 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation test_pickle_load[arm] 1.6 ms 1.8 ms -10.27%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing decimal-constraints-683 (3e7d11e) with main (3f67b34)

Open in CodSpeed

Footnotes

  1. 135 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

…string

The float branch already rejected inf and nan bounds; the Decimal branch
converted to double for the positivity check only and let Decimal('NaN')
and Decimal('Infinity') through. Also check PyObject_IsInstance for error.
@Siyet

Siyet commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Merged current main in, resolving the stub against the Meta overloads from #700: the five overloads are kept and the numeric params are retyped to int | float | Decimal | None.

Holding this PR. Re-reviewing it turned up a real defect of mine: decimal.InvalidOperation escapes json.decode on ordinary input. Annotated[Decimal, Meta(multiple_of=Decimal("3"))] with "1E+30" raises [DivisionImpossible], and Annotated[Decimal, Meta(gt=Decimal("1"))] with "NaN" raises InvalidOperation. Both float equivalents raise a clean ValidationError, so as it stands this PR throws a non-msgspec exception out of decoding untrusted input.

Fixed in this push: Decimal bounds now get the finiteness check the float bounds always had, and both that check and the multiple_of > 0 check run against an exact Decimal, so a subclass overriding is_finite or __gt__ cannot slip a NaN or a negative bound past them. That also fixed Decimal("1E+400") being rejected and multiple_of=Decimal("1E-400") being rejected as "must be > 0", both from routing the check through a double. The Meta C docstring no longer says "int or float".

Not fixed, because it is a semantics call that also decides how the remainder gets computed: for non-finite input, either follow float parity and let it fail the comparison as a ValidationError, or reject it outright on a constrained Decimal. Which do you want? @sobolevn @provinzkraut

This adds public API and touches TypeNodes, so it needs a real review rather than a merge on silence.

Александр Сергеевич Целуйко and others added 2 commits September 7, 2026 18:09
`ensure_is_finite_numeric` converted int bounds with `PyLong_AsDouble`
and never checked the result. For an int outside the range of a double
the `OverflowError` was left pending and the function still returned
true, so `Meta(gt=10**400)` surfaced as `SystemError`, and for
`multiple_of` the `-1.0` sentinel tripped the `<= 0` check and reported
a large positive bound as non-positive. On this branch the misreport got
worse: `Meta(gt=10**400, lt=Decimal("5"))` blamed `decimal.Decimal`.

Rejecting those ints is not an option here, because `Meta` already
treats the two spellings as one value: `Meta(gt=10**400)` and
`Meta(gt=Decimal("1E+400"))` compare equal, hash equal, and collapse to
a single object inside `Annotated`. Accepting one and refusing the other
would also contradict this branch's own argument that a double is the
wrong gate for large-but-finite bounds.

So drop the double entirely for ints. Every int is finite, and the only
remaining check, positivity for `multiple_of`, is done exactly against
zero, which is what the Decimal branch already does; that block is
factored out into `ensure_is_positive` and shared.

Whether a bound is usable stays a per-type question, decided where the
annotated type is known, as it already was for `Meta(gt=2**64)`: int64
for `int` targets, float64 for `float` targets, exact for `Decimal`
targets. `_constr_as_f64` grows the missing half of that, turning the
overflow into the same kind of message `_constr_as_i64` already gives
instead of relying on an invariant that only held because the bug
crashed first.

Co-Authored-By: Claude Opus 5 <[email protected]>
@Siyet

Siyet commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Pushed two things: current main merged in (this was four merges stale), and a fix for an overflow bug in ensure_is_finite_numeric that this branch made worse.

Why it belongs here rather than in its own PR. The unchecked PyLong_AsDouble in that function predates this work: on main today, Meta(gt=10**400) raises SystemError, and Meta(multiple_of=10**400) reports a large positive bound as must be > 0, because the -1.0 sentinel trips the positivity check. It is the same root cause as the convert_int bug fixed in #1162. What makes it this branch's problem is that the branch degraded it. On main:

>>> msgspec.Meta(gt=10**400, lt=Decimal("5"))
TypeError: `lt` must be an int or float, got decimal.Decimal

and here, before this push:

>>> msgspec.Meta(gt=10**400, lt=Decimal("5"))
SystemError: <class 'decimal.Decimal'> returned a result with an exception set

A typed error became a SystemError blaming decimal.Decimal, because the new Decimal branch now runs with a pending OverflowError.

Accept rather than reject. The obvious fix is to raise a ValueError for out-of-double int bounds, and I had that written before discarding it. It cannot be right here, because Meta already treats the two spellings as one value:

>>> Meta(gt=10**400) == Meta(gt=Decimal("1E+400"))
True
>>> hash(Meta(gt=10**400)) == hash(Meta(gt=Decimal("1E+400")))
True

Annotated is cached by equality, so Annotated[Decimal, Meta(gt=10**400)] and Annotated[Decimal, Meta(gt=Decimal("1E+400"))] are the same object. Rejecting one and accepting the other would also contradict this branch's own argument, that a double is the wrong gate for large-but-finite bounds, applied to ints instead of Decimals. And it would add a third representability threshold at construction time, so ge=10**308 would pass and ge=10**309 would not, even on a Decimal field where both work.

So the int branch stops going through a double at all. Every int is finite, and the only remaining check, positivity for multiple_of, is done exactly against zero, which is what the Decimal branch already did; that block is now shared as ensure_is_positive. PyLong_CheckExact keeps subclasses out of the comparison, so a lying __gt__ cannot slip past it.

Usability stays a per-type question, as it already was for Meta(gt=2**64), which this branch accepts and then refuses only for int targets. _constr_as_f64 grows the missing half: instead of the comment "should never be hit, types already checked" (an invariant that held only because the bug crashed first), an out-of-double bound on a float target now gets the same shape of message _constr_as_i64 already gives. The result:

bound int target float target Decimal target
10**400 doesn't fit in an int64 doesn't fit in a float64 works, exact

Verified on the merge with current main, CPython 3.14.6: unit suite 6466 passed, 143 skipped (26 new tests, no regressions); stubtest clean across 12 modules, which matters now that #1116 has landed and gates the stubs; mypy on tests/typing clean; ruff, format and codespell clean; docs build green with --fail-on-warning.

One cosmetic wart, disclosed rather than hidden: the int spelling produces Expected Decimal > Decimal('1000...000') with 401 digits, where the Decimal spelling gives Decimal('1E+400'), because _constr_as_decimal builds an unnormalised Decimal from the int. Harmless, and I would rather not normalise silently, but say the word if it bothers you.

Deliberately not in this push: ensure_is_nonnegative_integer has the same shape with PyLong_AsSsize_t, so Meta(min_length=10**400) claims a positive value is negative. No causal link to this branch and no design question in it, so it goes separately.


@sobolevn this is still held on the semantics question from my last comment, and it has grown a third case. All three, on this branch:

# 1. remainder against a bound that cannot divide
multiple_of=Decimal("3"), input "1E+30"   -> InvalidOperation: [DivisionImpossible]
# 2. comparison against NaN
gt=Decimal("1"),          input "NaN"     -> InvalidOperation: [InvalidOperation]
# 3. Infinity satisfies any lower bound, silently
gt=Decimal("1"),          input "Infinity" -> Decimal('Infinity')

The third is the worst of them: it is a wrong answer rather than an exception, and the float path disagrees, rejecting 1e400 with ValidationError: Number out of range. Note none of this depends on Decimal bounds; multiple_of=3 with a plain int bound reaches case 1 too, so it is about what the decoder does with non-finite and very large input, not about the bounds feature.

My inclination is that all three should be ValidationError, with Infinity failing every finite bound and NaN failing all of them, but that is a semantics call I would rather not make alone. Your review of the Decimal storage was the substantive one on this PR, so I would value your read on this in particular.

@provinzkraut your check that a Decimal bound is only valid on a Decimal type is still in and still enforced; the new table above is what happens to int bounds, which that guard does not cover.

@Siyet

Siyet commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

CI is green apart from CodSpeed, which is the known false flag, not something to look at.

The single regressed benchmark is test_pickle_load[arm], 1.6 ms to 1.8 ms. That is the bimodal one tracked in #1110: it flips between those two exact values on commits that do not touch the pickle path, including on main itself. Today it flipped the other way on #1116, a stubtest-only PR, where the same benchmark was reported as 1.8 ms to 1.6 ms, +11.48%. This branch touches Meta.__new__ argument validation and constraint setup, neither of which runs while unpickling a Struct.

Everything else passed: tests on 3.10 through 3.15-dev, all seven wheel builds, Pyodide, integration, docs, memory benchmarks and the Deploy preview.

sthagen pushed a commit to sthagen/jcrist-msgspec that referenced this pull request Sep 13, 2026
)

Calling `Meta(min_length=10**400)` results in a `ValueError` with the
incorrect message "must be >= 0" for a positive integer. With this fix,
out-of-range `min_length` and `max_length` values result in an error
identifying the range issue. Accepted values and diagnostics for
representable negative values are unchanged.

This is the length-bound fix discussed in msgspec#1122, separate from the
numeric constraints work in msgspec#1006.

Co-authored-by: Александр Сергеевич Целуйко <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support numeric constraints for Decimal values

4 participants