fix: enforce array contains/minContains/maxContains on generated models - #57
fix: enforce array contains/minContains/maxContains on generated models#57vishkaty wants to merge 1 commit into
Conversation
datamodel-code-generator drops contains/minContains/maxContains, so the generated Totals (a bare list[Total] alias) accepts arrays that violate totals.json's "exactly one subtotal and one total" rule. Snapshot the pristine schemas before preprocessing (which merges allOf and would drop the second contains), derive every predicate from contains.properties.*.const, and thread a pydantic AfterValidator enforcing all bounds into the alias metadata — base model and request variants alike. Also fix the moved description import so the SDK-guarded codegen tests run. Data-driven, idempotent, dependency-free.
1bd8d19 to
f1c4c44
Compare
|
Following up with the results of a full end to end verification. I regenerated the models against the pinned schema and ran the complete conformance suite against the reference flower shop server, comparing a build with these validators to a baseline without them. The baseline passes cleanly (69 tests, 0 failures). With the contains validators active, every checkout operation fails (67 failures across all 15 files). The cause is a construction order in the reference server, not a spec violation. The server creates the checkout with an empty totals list and then fills it in during recalculation: Because the constraint is an AfterValidator, it runs at construction time on the transient empty list and raises before recalculation runs. The final wire document the server emits is fully valid (exactly one subtotal and one total), so this is only about when the check fires. The wider point is one of altitude. The generated models are used both to parse incoming documents and to build outgoing ones incrementally. A wire level contains cardinality rule enforced on every construction rejects a legitimate mid build state, which makes it a breaking change for any builder style consumer, the reference server being the clearest example. This differs from a leaf constraint like the minProperties work, where the object is never built empty and then populated. Since you maintain both the SDK and the reference server, I did not want to pick a direction unilaterally. Two options that both preserve strict enforcement:
I am happy to implement either, or a variation you prefer. Marking this as a draft until we align on the approach. |
|
Thanks for drafting the change and outlining the options @vishkaty ! I'm good with option 1 |
|
Thanks - going with option 1. I filed the reference-server change as Universal-Commerce-Protocol/samples#152. It builds the in-progress checkout with Verified end to end: with samples#152 applied and the validators here active, the full conformance suite runs 69 passing / 0 failures (was 67 failures without the server change). samples#152 is safe to land independently - it behaves identically with or without these validators - so it can merge first or alongside this to keep the reference server green. Marking this ready for review. |
The SDK now enforces the totals contains cardinality (exactly one subtotal and one total) on the response models. create_checkout constructed the checkout with an empty totals list and filled it in during recalculation, so construction validated the empty list and rejected it, returning 500 on every create. Seed a valid placeholder (one subtotal + one total) at construction so the in-progress model satisfies the constraint; _recalculate_totals overwrites it with the server-authoritative amounts immediately after. Construction stays fully validated, and the emitted wire document is unchanged. Verified against the pinned SDK build with the totals validators active: full conformance suite 69 passing / 0 failures (was 67 failures). Also green without the validators: server integration tests (16) and the happy-path client, so it is safe to land independently. Relates to Universal-Commerce-Protocol/python-sdk#57
|
Small correction on approach: I moved samples#152 away from Re-verified: 69/0 conformance with these validators active, and the server's own integration tests (16) + happy-path client green without them. |
The SDK now enforces the totals contains cardinality (exactly one subtotal and one total) on the response models. create_checkout constructed the checkout with an empty totals list and filled it in during recalculation, so construction validated the empty list and rejected it, returning 500 on every create. Seed a valid placeholder (one subtotal + one total) at construction so the in-progress model satisfies the constraint; _recalculate_totals overwrites it with the server-authoritative amounts immediately after. Construction stays fully validated, and the emitted wire document is unchanged. Verified against the pinned SDK build with the totals validators active: full conformance suite 69 passing / 0 failures (was 67 failures). Also green without the validators: server integration tests (16) and the happy-path client, so it is safe to land independently. Relates to Universal-Commerce-Protocol/python-sdk#57
What
totals.jsonrequires an array that contains exactly onesubtotaland onetotalentry (allOfof twocontainsgroups, eachminContains:1/maxContains:1).datamodel-code-generator drops
contains/minContains/maxContains, so thegenerated
Totals(a barelist[Total]alias) accepts arrays that violate this —empty, missing a
total, or duplicatesubtotals all pass validation.This extends the existing
postprocess_models.pymechanism (added in #55 forminProperties) to also enforce array containment. Becausepreprocess_schemas.pymerges
allOfand a JSON node can hold only onecontains(silently dropping thesecond group), the injector now snapshots the pristine schemas before
preprocessing and derives every predicate from
contains.properties.*.const. Itthreads a
pydantic.AfterValidatorenforcing all bounds into the alias metadata —Totalsand its create/update request variants alike. Data-driven (nothinghardcodes
subtotal/total), idempotent, dependency-free.Also fixed: SDK-guarded tests were silently skipping
description.jsonmoved fromshopping/typestocommon/types, so the sharedtest import raised and set
HAVE_SDK = False— every SDK-guarded test (including#55's
minPropertiestests) was skipping in CI. This corrects the import, so thosetests run again.
Tests
tests/test_codegen_pipeline.py:TotalsContainsTest— acrossTotals,TotalsCreateRequest,TotalsUpdateRequest:reject
[],[total, total],[subtotal, subtotal],[subtotal],[total];accept
[subtotal, total]; plus a test asserting a subtotal-only array fails onthe
totalrule specifically.ArrayContainsInjectorTest— scanner reads bothallOfbranches and root-levelsingle
contains; ignores non-array / predicate-less; injection + idempotency.Verification
python -m unittest discover -s tests -p "test_*.py"):33 passed, 0 skipped (with the SDK-guarded tests now actually running; fix: enforce minProperties on generated models #55's
tests pass).
bash generate_models.shregenerates the validator on all three models;idempotent (byte-identical re-run); ruff + ruff-format clean.
Generated models are intentionally not committed (the tracked generated tree is
stale vs
main); the injector emits the validator whenevergenerate_models.shruns.