Skip to content

15 min read

Field descriptions versus system prompts

On this page

Every field in a JSON Schema can carry a sentence of prose under the key description, and in a request to a model that sentence is part of the program rather than a note about it. The schema is serialised into the request body — as a tool definition, a response format, or whatever the provider calls it — and the descriptions travel with it, so they are input the model conditions on in the same sense the prompt is. That makes the placement of every field-level rule a real decision: the same sentence about date formats can live in the system prompt or on the field, and those two are not equivalent.

The claim here is that a rule governing one field belongs on that field, and the evidence is a before-and-after on the same model, the same temperature and the same documents. A contract-extraction service carried All dates must be output in ISO 8601 format (YYYY-MM-DD). as bullet thirty-one of a 1,900-token system prompt, and over 500 contracts contract_effective_date came back in ISO form 91 percent of the time. The rule was deleted from the prompt and the six date fields in the schema each grew a description:

json
{
  "contract_effective_date": {
    "type": "string",
    "description": "The date the contract takes effect, as YYYY-MM-DD. If the contract says it takes effect on signature, use the signature date. Empty string if no date can be determined."
  }
}

Non-ISO output on the same 500 contracts fell to 0.4 percent — two responses, both of them contracts with no date at all where the empty-string clause had not been read as applying. The total instruction text got slightly longer rather than shorter. What changed is where the instruction was standing when the model needed it.

Both percentages are easier to hold as counts, and the counts are what make the comparison mean anything. Nine percent of 500 contracts is 45 responses that came back in some other date format. Nought point four percent of the same 500 is two. The denominator is identical, the documents are identical, and the same six fields were being asked for in both runs, so what separates the two figures is 43 responses that changed behaviour and nothing else about the setup. It is also worth noticing what the smaller number makes possible: at 45 you sample and generalise, and at two you read both of them, which is how the surviving pair were characterised precisely enough to name the clause they had missed.

A description is executable, which is not what the field is used for elsewhere

The description key is borrowed from JSON Schema and from OpenAPI, the specification format in which HTTP services publish the shape of their endpoints, and in both of those it has one job: it is documentation. A validator ignores it, a code generator turns it into a docstring, and at runtime it is stripped out entirely. Nothing you write in an OpenAPI description has ever changed the behaviour of a running system, which is why engineers inherit the habit of writing them last, or not at all, and of treating them as the place for notes to future maintainers.

In a request to a model, the same key is part of what is executed. Here is what actually leaves the client:

json
{
  "model": "vendor-mid-2026-02",
  "messages": [
    { "role": "system", "content": "Extract the fields from the contract." },
    { "role": "user", "content": "<contract text>" }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "contract_extract",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "contract_effective_date": {
            "type": "string",
            "description": "The date the contract takes effect, as YYYY-MM-DD. ..."
          },
          "governing_law_jurisdiction": {
            "type": "string",
            "description": "The jurisdiction named in the governing law clause, as written. Empty string if no such clause exists."
          }
        },
        "required": ["contract_effective_date", "governing_law_jurisdiction"],
        "additionalProperties": false
      }
    }
  }
}

Everything in that schema object is input the model conditions on, billed as input tokens, and cached along with the rest of the prefix if you are using a prompt cache, the provider-side mechanism that stores an unchanged leading section of a request and bills it at a reduced rate on subsequent calls. There is no separate channel where the shape goes and the prose does not. A description is therefore an instruction with a delivery address attached, and the address is the difference that matters.

Why bullet thirty-one loses to a sentence attached to the field

Generation is sequential, so at the moment the decoder is choosing the first token after "contract_effective_date":, two things are true. The description for that field is bound to the key that was just emitted, so no work is needed to establish that it applies. Bullet thirty-one is 1,400 tokens back in a list of forty rules, most of which govern other fields, so applying it requires the model to scan that list and decide which of the forty are in scope for the field currently being written.

That scan is a retrieval problem, and its error rate is not constant. With four bullets governing four fields it is nearly free. With forty bullets governing forty fields it has to be re-resolved forty times per response, once for each field, and each resolution is an opportunity to pull in a rule meant for a neighbour or to miss one that applied. The failure this produces is characteristic: rules near the top and bottom of a long list are followed more reliably than rules in the middle, and rules whose wording resembles several fields get applied to the wrong one. The nine percent non-ISO rate above was not the model being unable to write an ISO date; it was bullet thirty-one not being in scope at the instant the value was written.

That growth is worth being precise about, because it is linear in neither number. Four bullets governing four fields means each field is screened against four candidate rules, which is sixteen pairings across a response. Forty bullets governing forty fields is forty screenings repeated forty times, which is sixteen hundred pairings, and each one is a chance to bind a rule to the wrong field or to skip one that applied. Doubling the size of a schema therefore does not double the work of applying its prompt; it roughly quadruples it, because the number of values to be written and the number of rules each value must be screened against grow together. That is why a prompt behaving impeccably at six rules degrades at forty rather than degrading proportionally along the way, and why the degradation tends to arrive as a surprise.

The objection that usually arrives at this point is that the model reads the whole context anyway, so location cannot matter. It reads the whole context in the sense that every token is available to attend to, and that is the part of the claim that is true. What it does not follow from is that every token is equally likely to be attended to at a given step, or that the model has correctly worked out which of forty rules governs the key it is currently writing. Availability is not application. The 500-contract measurement is the cheap way to settle the argument in your own codebase: move the rules for one field group, hold everything else fixed, and count.

Attachment matters independently of distance. Even a short system prompt with six rules requires the model to bind each rule to a field by name, and names in prose are approximate — "all dates" has to be matched against contract_effective_date, termination_date, renewal_notice_deadline and last_amended, one of which is not obviously a date at all. A description on the field skips the binding step entirely, because the schema has already done it.

The rule, and where it stops

The rule a new hire can apply without judgement is that a rule governing exactly one field lives on that field, and everything else lives in the prompt. It is worth stating the second half properly, because "put everything in descriptions" is the overcorrection and it is nearly as bad.

Kind of ruleExampleWhere it goesWhy
Field format or unitdates as YYYY-MM-DDdescription on the fieldin scope exactly when the value is written
Field absence policyempty string when no clause existsdescription on the fieldthe boundary is field-specific
Field boundary or tie-breakheader figure wins over line itemsdescription on the fieldthe disambiguation is local
Cross-field consistencyend_date must not precede start_datesystem prompt, and validated in codeno single field owns it
Task framing"these are UK commercial leases"system promptapplies to every field equally
Output-wide policy"never infer facts not in the document"system promptapplies to every field equally
Anything a validator can checkIBAN checksums, enum membershipschema constraint, not proseprose has a failure rate; constraints do not

The last row is the one that gets argued about, and it is the boundary between this chapter and the material of topic 5.2, on constrained decoding. A description is applied by the model. A pattern, an enum, an integer bound, or a required-key list is applied by the decoder, which cannot emit a token that would violate it. Writing must be a valid ISO 4217 currency code in a description gets you the right answer most of the time; declaring an enum of the eleven currencies you actually handle gets it every time. When a description says something the schema could have said, the description is a weaker version of an available constraint, and chapter 4 works through what that difference costs in practice, comparing an enum against a description that lists the same values.

There is a version of this that goes wrong in the opposite direction, where the prose asks for something the constraint forbids. A severity enum of low | medium | high with the description use urgent for anything that has taken the site down describes a value the decoder can never emit. The model does not error and does not tell you; it picks whichever legal member the instruction leans towards, usually high, and the distinction the description was reaching for is gone. Ambiguous or over-reaching prose does not soften a constraint, it just bends the choice within it, and the result is a field whose behaviour cannot be predicted from either the schema or the description alone.

The reason it bends rather than breaks is that nothing is checking. A pattern, an enum or a required-key list is evaluated by the decoder, which can refuse a token. A description is evaluated by nothing at all: no component in the request path reads the prose, compares it against the constraint declared on the same field, and reports that the two disagree. So an instruction naming an impossible value does not fail. It is applied, as a pull, to a distribution over the values that remain possible, and the pull lands on whichever legal member the wording most resembles. The absence of an error is not evidence that a description was consistent with its schema. It is evidence that no part of the system was ever positioned to notice.

What actually happens when a bullet and a description disagree

This is the question worth being precise about, because the intuitive answers — "the more specific one wins", "the later one wins", "the system prompt wins because it is a system prompt" — are all wrong in the same way. They assume the conflict is resolved once, globally, and that the response will be consistent with whichever side won.

The third of the three deserves taking seriously rather than dismissing, because a real observation sits behind it. Providers do treat the system role differently: it is documented as the place for standing instructions, it is trained to be harder to override from user text, and an engineer who has read that infers, reasonably, that there is a precedence order somewhere in the machinery. There is not. What exists is a weighting learned during training, which makes a system message a strong prior rather than a rule with authority over other text. Nothing in the request path compares two instructions, decides which governs, and discards the loser — there is no evaluator, no error, and no record that a conflict occurred. There is a model producing one token at a time with both texts in its context, and at each field it produces whichever value the two of them together make likeliest. Per-field resolution is not a quirk of this example; it is the only thing the mechanism is capable of.

What happens is that the conflict is resolved per field, at generation time, and the result is a response that is internally inconsistent. A team that inherited a 40-field schema, with half the rules in a system-prompt list and half in descriptions, had a missing-value contradiction between the two. Bullet nine said Use an empty string for any value not present in the document. Eighteen of the forty fields had descriptions ending in null if not stated. Across 300 documents, the eighteen fields with descriptions returned null for absence 96 percent of the time. The twenty-two without returned "". Nobody had shipped a bug, exactly, and the response validated every time, but the consumer had to handle both sentinels on a per-field basis, and the mapping of which field used which was written nowhere except in the schema's description keys.

Those two counts add to the forty fields in the schema, and that is the finding rather than a coincidence. No rule anywhere decided which sentinel a field would use. There was a bullet applying to all forty, a description applying to eighteen, and the eighteen were whichever fields somebody had happened to write a sentence for at some point. So the effective contract — what a consumer has to test for in order to detect absence — was settled by the edit history of a schema file rather than by any decision about missing values, and the only place that settlement is recorded is which fields have a description key and which do not.

That is the real cost of splitting the rules across two places. The first-order problem is that one of the two rules loses. The second-order problem, and the expensive one, is that neither loses cleanly, so the schema's effective behaviour is a per-field function of where its rules happen to live. You cannot diff it, you cannot review it as a unit, and a change to the system prompt silently changes the behaviour of exactly those fields that have no description of their own.

The defence of consolidating into descriptions is therefore not that descriptions are magic. It is that a field-scoped rule stored on the field can be reviewed with the field, changed with the field, and tested with the field, and that a forty-bullet list has to be re-resolved against every field it might govern each time the model writes one. Schema Design • Prabal makes the same case from a Pydantic angle — Pydantic being the Python library that turns an annotated class into the JSON Schema the request carries — and is worth reading if your schemas are defined in Python, where descriptions live in Field(description=...) and are easy to leave empty.

Two practical limits keep the rule from becoming "write as much as you like on every field". The first is length. A description of one or two sentences is applied as a unit; a description of 120 words with six clauses and three exceptions reproduces the bullet-list problem inside a single field, because the model now has to work out which of its clauses is in scope for the value it is writing. If a field genuinely needs six rules, that is usually evidence the field is doing two jobs and should be two fields, which is chapter 5's territory, on enums that answer two questions at once. A useful ceiling in practice is about 40 words: the format, one example, and the absence case.

The second is cost, and it is smaller than people fear. Forty fields with 25-token descriptions is 1,000 input tokens per request, which at $3 per million is $0.003 a call — three dollars per thousand calls, irrelevant against the price of a retry, let alone the price of a wrong extraction reaching a customer.

That is two multiplications and a division, and the division is where the estimate usually goes wrong by a factor of a thousand in one direction or the other. Forty fields at twenty-five tokens each is a thousand tokens of description on every request. Provider prices are quoted per million tokens, so a thousand tokens is a thousandth of the unit being priced, and a thousandth of three dollars is three tenths of a cent. A thousand calls therefore cost three dollars in descriptions. Descriptions also sit in the schema, which is a stable prefix that does not vary between requests, so on any provider with prompt caching they are read at the cached rate after the first call. Deleting a description to save tokens is close to always the wrong trade; the reason to keep them short is that short instructions are followed more reliably, not that long ones are expensive.

A description is an instruction with an address. Where the rule governs one field, attaching it there removes a retrieval step the model would otherwise perform on every value it writes, and removes it from the review problem as well, because the rule and the thing it governs become one artifact. Where the rule spans fields or frames the task, the prompt is still its home.

None of this makes descriptions safe. A description is prose the model chooses to follow, so it can be vague, it can contradict the type it sits on, and it can quietly take over work that the type system should have been doing. The next chapter is about the last of those, which is the most damaging because it produces responses that are valid, plausible, and impossible for a consumer to interpret correctly.

Schema design for LLMs — read the current version at https://daviddominguez.dev/learn/en/schema-design-for-llms/field-descriptions-versus-system-prompts

Back to top