Skip to content
Harness Engineering2026-09-1013 min read

LLM Business Rule Validation: JSON Parses, Now What?

Check an LLM proposal against trusted stock, prices and budget after its fields validate, with explicit errors for wrong quantities and stale data.

Key takeaways

  • A syntactically valid JSON object can still name a quantity that violates the calling application's own inventory and budget facts.
  • Structural checks validate the declared fields; business checks compare a proposal against trusted facts the model cannot alter.
  • Compute required quantities independently from trusted stock records; reading the proposal first does not make its claims authoritative.
  • A repair loop that regenerates a proposal can fix a schema mistake but cannot fix a stale snapshot or an exceeded budget, which need human or system input.
  • An accepted draft is a validated proposal, not a completed purchase, and a passing schema check is not evidence that the business decision was correct.

Rod Rivera

Author

LLM Business Rule Validation: JSON Parses, Now What?

Rod's note — read with a pencil; the margins are for you.

U.S. teaching example: Amounts are fictional USD prices, stored as integer cents. This is a U.S. version of the shop exercise, not a currency conversion or a record of a real charge.

Five vanilla tubs pass every schema check. Lucy needs six.

Consider this hypothetical replenishment task. A supplier-replenishment assistant receives a JSON object: {"sku": "vanilla", "quantity": 5}. The sku field is a nonempty string. The quantity field is a positive integer within a sane range. Every type matches, every required field is present, nothing extra sneaks in. It passes a schema that requires those two fields and constrains quantity to an integer from 1 to 50.

It is also wrong. Lucy has two tubs of vanilla ice cream in stock and a target level of eight. She needs six, not five. The object that passed validation would leave her short by one tub if anyone acted on it.

This is the gap between two questions that sound similar but are not: "Is this JSON shaped correctly?" and "Is this the correct business decision?" A parser and a schema validator answer the first question. Answering the second requires comparing the proposal against facts the model was never given control over: current stock, target stock, price, and an approved budget. Confusing these two checks is a common way for a system that "looks tested" to still approve the wrong action. This article works through exactly where that second check has to live, what it needs to know, and what happens when a proposal fails it for different reasons.

LLM business rule validation needs trusted context

A JSON parser turns text into Python objects. A schema validator, such as Pydantic operating in strict mode, additionally checks that those objects match declared types and constraints without silently converting a string "6" into the integer 6. Pydantic's own strict-mode documentation notes that coercion behavior differs by field type and by whether the input arrived as JSON or as a Python object, so a check that holds for one field type should not be assumed to hold for another without checking the current version (Pydantic, strict mode docs, retrieved 2026-09-10).

The positive-integer check alone does not know the required stock level. A field constrained to integers from 1 to 50 accepts 4, 5 and 6; it rejects 500. The allowed range is a useful constraint, but it does not select which permitted value this shop needs today.

Pydantic can also perform business validation, including through validation context or custom model validators. You could generate a schema with an exact quantity constraint from a trusted snapshot. Those implementations still need the stock facts that establish six as the correct value. The distinction here is between checking a generic shape and checking a proposal against authoritative business context, not between what libraries are capable of doing.

Work through a hypothetical replenishment order

Consider a shop snapshot with three products, each carrying on-hand stock, a target stock level, and a price in cents.

SKUOn handTargetRequired max(0, target − on hand)Price per tubLine cost
vanilla286250 cents1,500 cents
chocolate1260300 cents0 cents
strawberry154275 cents1,100 cents

The required quantity for each product is max(0, target - on_hand), computed entirely from the trusted snapshot. Chocolate is already over its target, so its required quantity is zero and it drops out of the correct order. Summing the two nonzero lines gives a correct total cost of 2,600 cents, against a budget of 3,000 cents in this teaching scenario. This arithmetic does not involve the model at all. It is the reference answer the proposal will be checked against.

Compare four candidate drafts. The first two rows abbreviate the relevant line inside an otherwise structurally valid draft. Three candidates fail a business check; the fourth passes:

Candidate draft or line excerptShape checkBusiness checkResult
{"sku": "moon-cheese", "quantity": 3}passesSKU not in trusted catalogrejected: unknown SKU
{"sku": "vanilla", "quantity": 5}passesrequired is 6, not 5rejected: quantity mismatch
Full correct draft, total 2,600 centspassesexceeds a 2,000 cents budgetrejected: budget exceeded
Full correct draft, total 2,600 centspassesmatches required, within 3,000 cents budgetaccepted draft

Every rejected row here has valid JSON. None of them fails because a bracket is missing or a type is wrong. Each fails because ordinary Python, running after the schema check, compared the proposal against trusted data the schema validator never saw. Here the application performs business validation after structural validation. A custom validator could perform both, provided it receives the same trusted context.

This worked case is adapted from a teaching lesson in an existing course, Validate the Business Decision and Bound the Repair Loop, which builds the full Pydantic models, a PolicyError exception, and a bounded two-attempt repair loop around this exact shop scenario, including the exact-budget boundary and an all-stocked case with an empty accepted draft.

The mechanism: where each check happens

Where structural checks end and business checks begin

The image above shows the pipeline stages a proposal passes through: raw text becomes a parsed object, the parsed object is checked against a schema for shape, and only then is it compared against a separately loaded trusted snapshot to produce either an accepted draft or a named rejection.

Two properties matter here. First, the trusted snapshot is loaded independently of the proposal; the proposal cannot edit the budget or the catalog, because those fields simply are not part of its schema. Second, the comparison step at "E" is ordinary code, not another model call. It is a dict equality check between what the proposal asked for and what the snapshot arithmetic requires. Anthropic's engineering notes on writing tools for agents observe that how a tool's inputs, outputs, and failure states are structured shapes how reliably an agent can use it (Anthropic, writing tools for agents, 2025-09-11, retrieved 2026-09-10). Naming each rejection reason explicitly, rather than returning one generic "invalid" status, is that same idea applied to a business-rule boundary: a model retrying against a named unknown_sku error can behave differently than one retrying against a named budget_exceeded error, because the permitted recovery depends on the source of the error.

That distinction sets up a bounded repair policy. A schema type mistake, like sending quantity as the string "6" instead of the integer 6, can reasonably trigger one more attempt with feedback about the type error. An approved budget that is too small, or a snapshot that has gone stale because someone else already recorded a sale, cannot be fixed by asking the model to try again; no amount of regeneration changes an approved spending limit or restores a snapshot's currency. The course lesson referenced above bounds this at two total attempts and stops immediately, without retrying, on a stale snapshot or an exceeded budget, returning those to a human reviewer instead.

Building the check yourself

Separate trusted facts from the proposal

Define two separate schemas: one for the trusted snapshot (stock, target, price, budget, currency, a snapshot identifier) and one for the untrusted proposal (SKU and quantity per line, plus a free-text explanation). Keep budget and price fields out of the proposal schema entirely, so a proposal cannot supply or override them even by accident.

Compute the reference independently

Compute the reference answer from the trusted snapshot alone: for each item, required = max(0, target - on_hand), and sum required * price across items to get the reference total. This calculation must not call the model.

Compare exact quantities and total cost

Check duplicate SKUs before constructing a lookup, then compare the complete proposed quantity mapping against the reference: reject on any SKU not present in the trusted catalog, reject if the proposed quantities do not exactly equal the required quantities, and reject if the total exceeds the trusted budget. Return a distinct, named reason for each rejection rather than one generic failure.

If the proposal can supply its own trusted budget, even perfectly valid fields can authorize the wrong spend. Keep policy values in the application-owned input and reject unexpected policy fields in model output. An extra-field rule protects the declared boundary only if you actually validate the untrusted object before using it.

The following standard-library function isolates the quantity and budget checks. It assumes stock rows and order lines already passed type, range and required-field validation, and catalog SKUs are unique. The course lesson supplies that complete Pydantic boundary. This helper still checks duplicate proposal keys before dictionary construction, so an overwrite cannot hide a repeated line.

python
def check_business_rules(lines, stock, budget_cents):
    catalog = {item["sku"]: item for item in stock}
    keys = [line["sku"] for line in lines]
    if len(keys) != len(set(keys)):
        return "duplicate_sku"
    proposed = {line["sku"]: line["quantity"] for line in lines}
    if not set(proposed).issubset(catalog):
        return "unknown_sku"
    required = {
        item["sku"]: item["target"] - item["on_hand"]
        for item in stock if item["target"] > item["on_hand"]
    }
    if proposed != required:
        return "replenishment_mismatch"
    total = sum(quantity * catalog[sku]["price_cents"]
                for sku, quantity in proposed.items())
    if total > budget_cents:
        return "budget_exceeded"
    return {"status": "accepted_draft", "total_cents": total}


stock = [
    {"sku": "vanilla", "on_hand": 2, "target": 8, "price_cents": 250},
    {"sku": "chocolate", "on_hand": 12, "target": 6, "price_cents": 300},
    {"sku": "strawberry", "on_hand": 1, "target": 5, "price_cents": 275},
]
good = [{"sku": "vanilla", "quantity": 6},
        {"sku": "strawberry", "quantity": 4}]
assert check_business_rules(good, stock, 2600) == {
    "status": "accepted_draft", "total_cents": 2600
}
assert check_business_rules(good, stock, 2599) == "budget_exceeded"
print("Exact-budget draft accepted; one penny below rejected")

The helper does not fetch live stock, check snapshot freshness or purchase anything. Those are separate operations around this calculation. Bind the draft to the snapshot that was validated and recheck the relevant business conditions before execution; a correct result for an old snapshot does not authorize a current purchase.

A currency label is not an exchange rate

A customer request in one currency compared against a budget stored in another is a distinct failure from a business-rule mismatch. Do not let a model infer or apply an exchange rate as part of validation; obtain an authorized rate and rounding policy separately, or reject the mismatch outright.

Testing the boundary, not just the happy path

A validator that only ever sees valid, in-budget proposals has not been tested at its actual job. The boundary cases are where a schema check and a business check can be silently confused with each other. Two are worth working through explicitly.

For the 2600 cents reference total, the budget test uses > to reject an overrun. A budget of 2600 cents accepts; 2599 cents rejects. Test those adjacent values, since a test far above and far below the limit would not distinguish > from >=.

A 2600-cents required order is rejected under a 2599-cents budget and accepted as a draft at budgets of 2600 or 3000 cents. At zero required work: if every product is already at or above its target, the correctly computed reference order is an empty list of lines, costing zero. An empty proposal against that snapshot should be accepted. This case matters because it is easy to write validation code that treats an empty proposal as automatically suspicious, when in this business rule an empty order is sometimes the exactly correct answer. It is equally easy to confuse a legitimately empty catalog with a catalog that failed to load; those are different failures and should not share a code path, since a failed read masquerading as "nothing to order" is a much worse bug than either failure alone.

Quick check — A proposal has a quantity field that is a valid positive integer. What does that prove about the proposal?

Check the decision

What changes when the schema carries policy

A schema built from trusted stock could constrain vanilla quantity to exactly six. That would reject five during schema validation. It would implement the business rule at a different location, rather than remove the need for trusted state. The application must still show where the value six came from and which snapshot the generated constraint represents.

Likewise, a Pydantic model validator can compare a draft with application-supplied context. The course uses an explicit function to keep those inputs visible. Either approach can work; choose one whose data flow and failure cases the receiving engineer can inspect. Test wrong quantities, duplicate lines, unknown products, stale snapshots and exact-budget equality under the actual boundary you chose.

A successful local calculation establishes the result for its supplied data. It does not demonstrate that a production database supplies fresh records, that the execution step uses the same snapshot, or that a supplier accepted an order. Keep the accepted draft and the execution receipt distinct.

A usable next action

Before wiring an LLM proposal into any action that spends money, changes inventory, or commits to a customer, write down the reference calculation in plain code first, independent of any model output. Then write the comparison step as a named-error function, the way the worked example above does, so a schema failure, an unknown SKU, a quantity mismatch, and a budget overrun each produce a distinct, loggable outcome rather than one undifferentiated rejection. Only after that separation exists does it make sense to ask whether a bounded number of regeneration attempts is worth adding, and for which of those named errors regeneration could plausibly help.

Work through the full repair-loop lesson with runnable fixtures

The referenced course lesson builds this exact validator, its bounded two-attempt repair loop, and a transfer exercise that adds a new product without moving the budget.

Ready to put an agent to work?

Join the Prof Rod newsletter for one educational lesson a week, with worked examples attached. It is free to register for and separate from the Zero Employee community.