AllOf Merge Conflicts Widen Numeric Bounds

Your validator says the payload is valid. Your test says the field is in range. Your production incident report says a charge of $0.00 processed successfully. The culprit isn't your business logic — it's a JSON Schema allOf composition where two subschemas defined conflicting minimum values, the validator took the looser constraint, and nobody noticed because no assertion failed. Schema keywords that silently widen validation are a well-documented class of problem, but the numeric-bounds variant is especially treacherous because the widening is invisible at the schema level and the data still looks typed correctly.

The technical core: JSON Schema 2020-12 does not merge allOf subschemas into a single unified constraint set. Each subschema is evaluated independently, and a value passes if it satisfies all of them — which means the effective bound is the most permissive minimum across all subschemas when those subschemas are authored inconsistently. Add a code-generated schema fragment from a third-party SDK alongside a hand-authored contract, and you have a conflict you will not see until money moves incorrectly.

By the end of this article you will be able to identify conflicting numeric bounds in composed schemas programmatically, write a pytest fixture that surfaces the widening before CI passes, and understand the two architectural decisions that make teams vulnerable to this class of failure.

Build an API Automation Framework With Node.js

Learn Node.js, Cucumber, GitHub Copilot, APIs, CI/CD, and modern automation by building a complete framework.

Learn more

Why AllOf Doesn't Intersect Numeric Constraints

In JSON Schema 2020-12, allOf is a logical AND over independent schema evaluations, not a constraint-merging operation. When subschema A declares "minimum": 1 and subschema B declares "minimum": 0, a value of 0 satisfies B and fails A — so the overall allOf correctly rejects it. But when subschema A declares "minimum": 1 and subschema B declares "minimum": 0 with no upper bound and subschema A also carries no maximum, a value of 0 still fails. The silent widening happens in the opposite direction: A says "minimum": 0, B says "minimum": -100. Now 0 passes both, but so does -50. The effective lower bound is -100, not 0. No validator error. No test failure. Just wrong data.

This matters in test data engineering because generated datasets — whether from Faker, Mimesis, or a factory_boy factory — are often validated against composed schemas to confirm the generator is producing legal values. If the schema itself is silently permissive, your generator will happily emit out-of-domain values that pass validation, seed your test database, and make downstream assertions meaningless. The problem compounds when schemas are split across $ref chains or pulled from separate OpenAPI component files, which is the default pattern in any service that shares a schema library.

Detecting and Locking Down Conflicting Bounds in CI

The first step is extracting the effective numeric bounds from a composed schema before you trust it for data generation or assertion. The jsonschema library (Python, v4.x) does not expose a merged-constraint view, so you need to walk the schema tree yourself. Here is a minimal extractor that surfaces conflicting minimum / maximum declarations across allOf members for a single property path:

import json
from pathlib import Path
from typing import Any

def collect_numeric_bounds(schema: dict, prop: str) -> list[dict]:
    """Return all minimum/maximum declarations for `prop` across allOf members."""
    bounds = []
    top = schema.get("properties", {}).get(prop, {})
    if "minimum" in top or "maximum" in top:
        bounds.append({"source": "root", "minimum": top.get("minimum"), "maximum": top.get("maximum")})
    for i, sub in enumerate(schema.get("allOf", [])):
        p = sub.get("properties", {}).get(prop, {})
        if "minimum" in p or "maximum" in p:
            bounds.append({"source": f"allOf[{i}]", "minimum": p.get("minimum"), "maximum": p.get("maximum")})
    return bounds

def effective_bounds(bounds: list[dict]) -> dict:
    mins = [b["minimum"] for b in bounds if b["minimum"] is not None]
    maxs = [b["maximum"] for b in bounds if b["maximum"] is not None]
    return {
        "effective_minimum": max(mins) if mins else None,  # allOf = most restrictive
        "effective_maximum": min(maxs) if maxs else None,
        "declared_minimums": mins,
        "declared_maximums": maxs,
        "conflict": len(set(mins)) > 1 or len(set(maxs)) > 1,
    }

schema = json.loads(Path("payment.schema.json").read_text())
b = collect_numeric_bounds(schema, "amount")
print(effective_bounds(b))

The conflict flag is what you gate on in CI. Wire this into a pytest parametrize over every numeric property in your schema registry and you get a schema-lint step that runs in under two seconds. On one payments platform schema with 14 allOf-composed subschemas, this script surfaced three conflicting minimum declarations that had been introduced by an auto-generated Pydantic model export — none had ever caused a validator rejection because the auto-generated bound was always the looser one.

For the generation side, once you know the effective bounds, enforce them explicitly in your factory rather than relying on the schema validator to catch violations after the fact. With factory_boy:

import factory
from faker import Faker

fake = Faker()

AMOUNT_MIN = 1       # effective minimum after allOf resolution
AMOUNT_MAX = 999999  # effective maximum after allOf resolution

class PaymentFactory(factory.Factory):
    class Meta:
        model = dict

    amount = factory.LazyFunction(
        lambda: fake.pydecimal(
            left_digits=5, right_digits=2,
            min_value=AMOUNT_MIN, max_value=AMOUNT_MAX, positive=True
        )
    )

Pinning the constants at the factory level means a schema conflict no longer silently propagates into generated data — any divergence between the schema's effective bounds and the factory's constants becomes a code review artifact. Pair this with a Hypothesis @given strategy that samples from st.decimals(min_value=AMOUNT_MIN, max_value=AMOUNT_MAX) and run it against your validator; Hypothesis will find the boundary edges the factory misses. Understanding the full range of numeric data types in test data engineering — precision, scale, integer vs. decimal — is a prerequisite for setting those constants correctly.

Where Senior Engineers Still Get Burned

The most common mistake is trusting schema validation output as a correctness signal for generated data rather than a conformance signal. These are different things. A validator confirms a value satisfies the schema as written; it says nothing about whether the schema as written reflects the domain invariant. When a schema has been widened by an allOf conflict, the validator is faithfully reporting conformance to a broken contract. Teams that skip the bound-extraction step and go straight to "validator passed, data is good" are building on that broken contract. The fix is treating schema linting as a separate, earlier gate — run it at schema-commit time, not at test-run time.

A subtler failure is relying on numeric coercion behavior in JSONPath assertions to catch out-of-range values. If your assertion framework coerces a string "0" to integer 0 before comparing against a minimum of 1, the assertion may still pass depending on comparison semantics. The root cause is usually a schema that allows both integer and number types across different allOf members — a type-union conflict that accompanies the bounds conflict. Audit for type mismatches at the same time you audit for bound mismatches.

Myths That Keep This Bug in Production

Myth 1: "If the schema compiles and the validator passes, the constraints are correct." Validators implement the spec, and the spec says evaluate subschemas independently. A schema with contradictory bounds across allOf members is spec-compliant and validator-passing; it is just semantically wrong. Schema correctness is a separate property from schema validity, and no mainstream validator (Ajv, jsonschema, Pydantic v2) will flag a conflict — they are not designed to. Myth 2: "Randomized generation provides coverage of boundary conditions." Standard random generation from Faker or Mimesis uses uniform distributions that almost never hit exact boundary values. Choosing between static and random test data matters here: static boundary fixtures (exactly minimum, exactly minimum - 1, exactly maximum, exactly maximum + 1) are what catch widening bugs, not random mid-range values.

Myth 3: "The schema came from the SDK, so it's authoritative." Auto-generated schemas from Pydantic model exports, OpenAPI generators, or gRPC-to-JSON-Schema converters frequently carry default numeric bounds that reflect the host language's type system (e.g., float min/max) rather than the business domain's invariants. Treating SDK-generated schema fragments as ground truth and composing them into allOf without auditing their numeric constraints is the single most common source of silent bound widening in microservice test suites. Always diff the generated fragment's numeric bounds against your domain spec before merging.

AllOf bound conflicts are cheap to find and expensive to ignore. Add the bound-extraction script to your schema CI step, pin factory constants to the resolved effective bounds, and run Hypothesis at the boundary edges. If you inherit a large schema registry, start with the properties that map to money, counts, or identifiers — those are where widened bounds cause incidents. The conflict flag in the extractor above is a one-line gate; there is no good reason not to have it.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles