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 |
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.