Writing Input Validation With LLMs in November 2026: Collect Real Bad Inputs First, Validate at the Boundary, and Test Every Rejection

Most production bugs that start with "a user sent something weird" come down to missing or sloppy input validation. A form accepted a negative quantity. An API took an email field holding 4,000 characters. A CSV import let a date like 31/02/2026 through, and three reports broke a week later. Language models are quick at writing validation code: give one a description of a field and you get a Pydantic model, a Zod schema, or a JSON Schema in seconds. The catch is that a model writes the rules it imagines your data follows, not the rules your data actually needs. This guide shows how to use an LLM to write validation that holds up, by feeding it real inputs, putting the checks in the right place, and proving each rule with tests.

What Validation Is For (and What It Is Not)

Validation answers one question: is this input acceptable for this operation, yes or no, and if no, why? It is worth being clear about what it does not do before you prompt anything:

  • It is not security by itself. Validation reduces attack surface, but you still need parameterized SQL queries, output escaping, and authorization checks. A model that "fixes" SQL injection by adding a regex to the input is giving you the wrong fix.
  • It is not business logic. "Quantity must be a positive integer" is validation. "This customer can only order 5 units because of their plan" belongs in the service layer, where it can see the account.
  • It is not cleanup. Trimming whitespace or lowercasing an email is normalization. It is fine to do both, but decide the order on purpose: normalize first, then validate the normalized value.

Telling the model these boundaries up front stops it from producing a single giant schema that tries to do everything.

Flour in a kitchen sieve

A sieve only works if the mesh is the right size. Validation rules need the same care: not so tight that good input gets stuck, not so loose that lumps get through. Image: Mehl in Küchensieb by IgorCalzone1, CC BY-SA 4.0, via Wikimedia Commons.

Step 1: Collect Real Inputs Before You Prompt

The biggest improvement you can make is to stop describing your data and start showing it. Before you ask for a schema, gather:

  1. A handful of real valid payloads, with personal data replaced by realistic fakes. Include the boring ones and the unusual-but-legal ones: a name with an apostrophe, an address with no second line, a phone number with a country code.
  2. Real bad inputs you have already seen. Look in your error logs, support tickets, and bug tracker. Empty strings where you expected null, numbers sent as strings, timestamps without a time zone, emoji in a username field.
  3. The consumers of the data. If the value ends up in a database column, note the column type and length. If it is sent to a payment provider, note that provider's documented limits. These are the real constraints, and models routinely invent tighter or looser ones.

A prompt built from this looks something like:

Write a Pydantic v2 model for this signup request.
Valid examples: [three JSON payloads]
Inputs we must reject, with the reason: [five JSON payloads + reasons]
Constraints from storage: email is VARCHAR(254), display_name is VARCHAR(50),
country is an ISO 3166-1 alpha-2 code.
Do not add rules that are not supported by the examples or constraints.
For each rule, add a one-line comment saying which example or constraint it comes from.

The last two lines matter. Asking the model to cite the source of each rule makes invented rules easy to spot during review. If a comment says "common practice" instead of pointing at an example or a constraint, question that rule.

Step 2: Name the Library and Version

Validation libraries change their APIs between major versions, and models mix them up often. Pydantic v1 and v2 use different decorators for custom validators. Zod, Joi, Yup, and JSON Schema drafts all express "optional" and "nullable" differently. Say exactly which library and version you use, and paste one existing schema from your codebase as a style reference. Then run the code. If it fails to import or a decorator does not exist, that is a version mix-up, not a reason to switch libraries.

Step 3: Validate at the Boundary, Once

Ask the model where the check should live, but decide it yourself. The usual answer is: validate where untrusted data enters your system, which means HTTP handlers, message queue consumers, file importers, and webhook receivers. After that point, the rest of the code works with a typed, already-checked object.

Two patterns to push back on when a model suggests them:

  • Client-side only. Browser validation is good for user experience, but anyone can send a request straight to your API. Server-side validation is required; client-side is optional extra.
  • Validation sprinkled everywhere. If the same email check appears in the controller, the service, and the repository, the three copies will drift apart. One schema at the edge is easier to keep correct.

Step 4: Watch for the Common Model Mistakes

When reviewing generated validation code, check these specifically, because they come up again and again:

Overly clever regexes

Email validation is the classic example. Models like to produce long regexes that reject legal addresses (plus signs, newer top-level domains) or accept broken ones. A practical rule is: check for a reasonable length and a single @ with something on both sides, then confirm the address by actually sending a verification email. Many libraries ship an email type that is better tested than anything the model writes inline.

Confusing missing, null, and empty

A field that is absent, a field set to null, and a field set to "" are three different inputs. Decide what each one means for each field and say so in the prompt. A PATCH endpoint, for example, often needs "absent" to mean "leave unchanged" and null to mean "clear this value".

Silent type coercion

Some libraries will quietly turn "12" into 12 or "false" into True, depending on mode. Pydantic has a strict mode; Zod has z.coerce as an explicit opt-in. Decide whether you want coercion and make the generated code say so explicitly, rather than relying on defaults the model may have remembered wrong.

Missing upper bounds

Models are good at minimums and forget maximums. Every string needs a maximum length, every list needs a maximum number of items, and every number needs a sensible range. Without them, a single request can send a 50 MB string into your database or a list of a million items into a loop.

Unknown fields

Decide whether extra fields are rejected, ignored, or kept. Rejecting them catches client typos like emial early. Ignoring them is friendlier for evolving APIs. Keeping them by accident can let a client set something like is_admin if the object is later passed straight to an ORM, so make this a conscious choice.

A full-height metal turnstile at an entrance

Check inputs once, at the entrance, so everything inside can trust what got through. Image: Full-Height Turnstile - TRS 370 - France by Automatic Systems, CC BY 2.0, via Wikimedia Commons.

Step 5: Turn Every Rule Into a Test

Treat the bad inputs you collected in Step 1 as a test suite. Ask the model to write parameterized tests that feed each valid example and assert it passes, and feed each invalid example and assert it fails with the expected error on the expected field. That last part is important: a test that only checks "validation failed" will keep passing even if the input is rejected for the wrong reason.

@pytest.mark.parametrize("payload, field", [
    ({"email": "", "display_name": "Ana", "country": "PT"}, "email"),
    ({"email": "a@b.co", "display_name": "x" * 51, "country": "PT"}, "display_name"),
    ({"email": "a@b.co", "display_name": "Ana", "country": "Portugal"}, "country"),
])
def test_rejects_bad_signup(payload, field):
    with pytest.raises(ValidationError) as exc:
        SignupRequest(**payload)
    assert exc.value.errors()[0]["loc"] == (field,)

Then add boundary cases the model should generate for you: exactly the maximum length, one over it, zero, negative one, the largest allowed number. Boundary bugs (using < where you meant <=) are the most common mistakes in hand-written and generated validators alike.

If your stack supports it, property-based testing is a good next step. Tools such as Hypothesis for Python or fast-check for JavaScript generate many random inputs and are good at finding the odd case nobody listed, like a string of only whitespace or a Unicode character that changes length when normalized.

Step 6: Write Error Messages People Can Act On

Validation errors are read by people: a user filling in a form, or a developer integrating with your API. Each error should name the field, say what was wrong, and say what is allowed. "display_name must be 50 characters or fewer (got 63)" is useful. "Invalid input" is not. Ask the model to produce messages in that pattern, and make sure they never echo back secrets such as passwords or full card numbers. For APIs, return all field errors at once in a structured list, so a client can fix everything in one round trip instead of one error per request.

Step 7: Keep Schemas and Reality in Sync

Validation drifts. A column gets widened, a new country code becomes valid, a partner starts sending a field you did not expect. A few habits keep it honest:

  • When you change a database column or an external limit, update the schema and its tests in the same pull request.
  • Log rejected inputs (with sensitive values redacted) and review them now and then. A spike in rejections on one field often means your rule is wrong, not that users are.
  • If you publish an API, generate your OpenAPI or JSON Schema document from the same source as the runtime validator, so the documentation cannot disagree with the code.

A Short Checklist

  1. Gather real valid and invalid inputs plus storage and partner limits before prompting.
  2. Name the exact library and version, and give a style example from your codebase.
  3. Ask the model to cite where each rule comes from, and remove rules with no source.
  4. Validate once, on the server, where untrusted data enters.
  5. Check for regex overreach, null versus missing confusion, silent coercion, missing maximums, and unknown-field handling.
  6. Turn every example into a test that checks the failing field, and add boundary cases.
  7. Write errors that name the field and the allowed values, without echoing secrets.

An LLM can save you most of the typing in validation work. The judgment about what your data is really allowed to look like still has to come from your own data, your storage limits, and tests that fail when a rule is wrong.

Comments

Popular posts from this blog

Grok Bot - a step closer to AGI

Structured Outputs and Constrained Decoding for Reliable LLM APIs in Production (September 2026)

Tencent Hy4 Preview: Open 770B MoE Built for Real Work