Chapter 1 of 7 · 16 min read
Naming a schema field
On this page
A model call that feeds a system rather than a person has two halves. One is the
prompt: the source text, and an instruction to pull something out of it. The
other is a schema — a document written in JSON Schema, the vocabulary for
describing the shape of JavaScript Object Notation data, listing every field the
answer must contain, giving each one a name and a type, and optionally attaching
a sentence of description to it. The provider takes that schema as a
response_format, or as the parameter block of a tool the model is allowed to
call, and topic 5.2, on constrained decoding, covered what happens next: the
decoder is held to the schema token by token, so the reply arrives as an object
the caller can deserialise rather than prose someone has to parse. That is the
problem a schema exists to solve, since a large language model (LLM) produces
text and a purchasing system needs a number sitting in a named slot.
What the constraint does not cover is which fields you ask for, what you call them, what type you give them, which values you permit, and how deeply you nest them. Those choices are schema design, and they decide what value arrives in a slot the decoder has already guaranteed will be filled and syntactically valid. Any call of this kind carries them: extraction pipelines, classifiers sitting behind a queue, and the tool definitions an agent chooses from, whose arguments are a schema like any other. This chapter is about the first choice, the name, which is the cheapest part of a schema to change and the part standing closest to the value at the moment it is written.
Here is a field that ran in a purchase-order extraction for a month:
{
"discount_pct": {
"type": "number",
"description": "discount percentage, e.g. 0.15 for 15%. Use 0 if none."
}
}Two separate things in that declaration work against the extraction, and they
belong to different chapters. The description offers zero as a legal answer
whenever the model is unsure, which chapter 3 takes up, on descriptions that name
a literal value the type does not have. The name is this chapter's subject.
discount_pct does not say whether a fifteen percent discount is written as 15
or as 0.15, and the abbreviation is used in production systems for both
conventions with roughly equal frequency — a discount_pct column in a billing
database is as likely to hold 15 as 0.15, and every engineer reading this has
been bitten by the ambiguity at least once in SQL. A model reading 15% off and
writing into a field called discount_pct has two defensible outputs, and the
name gives it nothing to choose with.
The name is the last thing the model reads before the value
The reason names carry weight is positional. A structured response is a token
stream, and the object is generated left to right, so by the time the model
produces the first token of the discount value it has already emitted
"discount_pct": — or, under constrained decoding, has had those tokens forced
into the sequence by the grammar. Either way the key sits in the immediate left
context of the value, closer than the system prompt, closer than the source
document, closer than the field's own description, which was serialised into the
request far away from the point of generation. Conditioning is not distance-blind.
The tokens right before the one being sampled shape the distribution more than
tokens ten thousand positions back, which is why a name that pulls one way and a
description that pulls the other does not resolve cleanly in favour of the longer
text.
Two claims are bundled together there and they are not equally strong, so they are worth separating before either is relied on. The weaker one is about raw distance — that nearby tokens count for more than distant ones. That holds for these models in aggregate and it is not a law. A model attends to whatever it has learned to attend to, and a distant instruction it has learned to weight heavily can beat a nearby one.
The stronger claim survives without the weaker one, and it is the one doing the work.
A description has to be bound to a field before it can be applied. The model reads
discount percentage, e.g. 0.15 for 15% somewhere inside the schema block and has to
establish that this sentence, rather than the sentence attached to one of the other
fields, governs the value it is about to write. That is a step, and steps have error
rates. A key requires no such step. It is not a sentence mentioning the field; it is
the field, sitting in the position immediately before the value, present on every
response, and unambiguous about its referent because there is nothing else it could
refer to. The name wins not by being louder but by arriving already resolved.
This is measurable, and it has been measured under exactly the condition that makes it counterintuitive. Schema Key Wording as an Instruction Channel in Structured Generation under Constrained Decoding looks at what happens when the keys themselves are not free choices — the decoder is emitting them because the grammar says so, not because the model decided to — and finds that the wording still shifts the values that follow. The key is forced, and it conditions anyway.
The comparison is cheap enough to run on your own data, and one procurement
team's run of it is the shape to expect. They took 200 purchase orders whose
text read some variant of "15% off", held the description constant at
discount percentage, e.g. 0.15 for 15%, and varied only the field name across
three runs against the same model:
| Field name | Emitted 0.15 | Emitted 15 | Emitted 0 |
|---|---|---|---|
discount_pct | 96 | 71 | 33 |
discount_percent | 41 | 152 | 7 |
discount_fraction | 188 | 3 | 9 |
Two properties of that table are worth checking before anything is read off it. Every row sums to 200, so nothing failed and nothing was dropped: each of the 200 documents produced exactly one value under each name, which is what makes the three runs three measurements of one quantity rather than three differently-shaped samples. And the description, the documents, the model and the temperature were held identical across the runs, so the name is the only variable that moved, and the differences between the rows have nowhere else to have come from.
The description said 0.15 in all three runs. With discount_percent in front
of it, the model went the other way three times out of four, because "percent"
reads as the unit of the number and a number in percent is 15. With
discount_fraction, the name and the description agree, the ambiguity
disappears, and the residual nine zeroes are documents where the discount
genuinely was not stated. The name did not merely fail to help in the first two
runs; it competed with the description and won more often than it lost.
The zero column carries a second finding that is easy to walk past. Thirty-three
documents came back as 0 under discount_pct and nine under discount_fraction,
over the same source text — so more than twenty of those zeroes cannot be documents
without a discount, because the same documents produced only nine zeroes when the name
was unambiguous. They are documents where the model, handed a name it could read two
ways, took the answer the description had helpfully declared legal. An ambiguous name
does not only split the mass between the two readings of the value; it pushes some of
it onto whatever safe answer is on offer elsewhere in the declaration. That failure is
what chapter 3 takes apart, on descriptions that name a literal value, and what
matters here is that the name is what triggered it.
The unit result gives a practical rule about which unit words are worth putting in a
name.
Some are self-defining and settle the question on their own: fraction, _usd,
_cents, _ms, _seconds, _iso, _count. Others look like units and are
not: pct, rate, amount, time, size, duration. A field called
response_time could be milliseconds or seconds and a model will guess from the
magnitudes in the source; response_time_ms cannot be read two ways. The
cheapest fix available in schema design is to move the unit out of the prose and
into the identifier, where it is in the model's left context at the moment it
matters and, incidentally, where every human reading the downstream code will
also see it.
Names that assert what the source does not establish
The unit problem is the tractable half. The harder failure is a name that commits the model to a claim before the description gets a chance to qualify it, because the grammatical form of the name is itself an assertion.
Take is_exploitable on a security-triage schema, declared as a boolean. A
researcher's report says the tenant identifier is read from a request header and
never checked against the session. Is that exploitable? It depends on whether the
header is stripped at the edge proxy, on whether the endpoint is reachable
without authentication, on whether a second control catches it downstream — none
of which the report says, because the researcher did not have access to the
deployment. The field nonetheless has two legal values and both of them are a
claim about the world. false says the vulnerability cannot be exploited, which
the report does not support. true says it can, which the report also does not
support. There is no legal way to answer honestly, so the model answers
confidently, and roughly whichever way the report's tone leans.
The chain there is short and every link is checkable. A boolean type has exactly two inhabitants. Constrained decoding requires one of them at that position, so abstention is not among the legal continuations. Both inhabitants are assertions about a deployed system. The report establishes neither. The model must emit anyway, and with nothing in the input to settle the matter, what settles it is whatever weak signal is available — the report's register, the researcher's confidence, the word critical sitting in a heading.
The repair most reviewers reach for is a firmer description: only answer true if you
are certain the issue is exploitable in production. It is worth being precise about
why that does not work, because the instinct behind it is sound and the mechanism
defeats it. A description cannot add a value to a type. Once the model has read the
instruction and concluded that it is not certain, the grammar still admits exactly
true and false, and one of them has to be emitted. The instruction can shift which
one, usually towards false. Shifting the answer is not the same as recording the
uncertainty, and a false produced by a model that meant I cannot tell is
indistinguishable, downstream and forever, from a false produced by a model that
checked. Every repair that works changes the set of legal answers rather than the
prose around it: a third value, a nullable type, or a different question.
The tell is that is_ and has_ names describe reality, and an extraction
schema is usually in a position to describe only a document. Moving the claim
from the world to the text makes the field answerable:
{
"report_claims_exploitable": {
"type": "boolean",
"description": "true only if the report explicitly states or demonstrates that the issue can be exploited in a deployed environment. A proof-of-concept counts. A theoretical description of the flaw does not."
}
}Nothing about the model changed and nothing about the underlying uncertainty
changed. What changed is that report_claims_exploitable asks about something
present in the input, so both answers can be grounded, and a report with no
exploitation claim now has a correct answer instead of a coin flip. The
downstream consumer loses the thing it wanted — a verdict on exploitability — but
it was never getting a real one, and it is better off knowing that a human has to
supply it.
The same shape appears in quieter places. primary_contact_email on a schema fed
contracts that list three signatories, none of them marked primary, asks the
model to nominate one and then reports the nomination as though the document made
it. final_price on quotes that are explicitly non-binding does the same. Both
names are true on the common case, which is why they survive review: someone
tested on ten documents where there really was one contact, the field worked, and
the name went in. The failure only appears on the tail, where the name quietly
requires a false statement, and on the tail there is no signal in the output that
anything went wrong. first_listed_contact_email and quoted_price are worse
names in the sense that they promise less, and better fields in the sense that
they can be satisfied.
Here is the pattern across the four names in this chapter:
| Name | What it commits the model to | What it leaves open | Answerable from the document |
|---|---|---|---|
discount_pct | that a discount number exists | the unit, and what absence looks like | partly |
discount_fraction | a value in 0–1 | what absence looks like | yes |
is_exploitable | a verdict about a deployed system | nothing — both values are claims | no |
report_claims_exploitable | a reading of the report's text | nothing material | yes |
Three limits on what a name can carry
Renaming is cheap and it is not powerful. It relocates a question; it does not
answer one. report_claims_exploitable gives you a field the model can fill
honestly, and you still do not know whether the vulnerability is exploitable. If
that verdict is what the business needs, the schema is not where it comes from —
it comes from a second input, a scanner result or a config dump or an engineer,
and no amount of naming discipline substitutes for the missing evidence. A schema
review that ends with "we renamed the field" when the real problem was that the
model cannot know the answer has moved a bug rather than fixed one.
Names also have a hard length budget, and it is short. A name gets you the
referent and the unit, and that is close to all it gets you, because
discount_fraction_of_list_price_excluding_tax_and_freight is a description
wearing a name's clothes: unreadable in the calling code, awkward as a database
column, and long enough that its tail tokens sit far enough from the value to
stop behaving like the immediate context that made short names work in the first
place. That last reason is the weakest of the three and should not be leaned on by
itself, since it rests on the distance claim narrowed above; the other two hold on
their own and are enough. Anything that takes a clause — how to handle multiple discounts, which of
two stated figures wins, what to do when the percentage and the absolute amount
disagree — does not fit and should not be forced.
There is a related habit that costs more than it looks like it should, which is
the house abbreviation. Schemas that grew out of a database tend to carry amt,
qty, dt, ts, cd, nbr, desc, and each of those is a name that has been
compressed past the point where it identifies anything on its own. dt is a date
in one team's convention and a delta in another's; desc is a description to
most readers and a sort direction to anyone who has written enough SQL; cd is a
code, or a compact disc, or a change directory. A human resolves these instantly
from context because they have read the surrounding table a hundred times. A
model resolves them from the token sequence in front of it, which contains the
abbreviation and not the hundred readings, so an abbreviation converts a name
that was doing work into a name that is doing almost none. Expanding them is the
single highest-yield edit on an inherited schema and it costs nothing at the
model's end, because the extra tokens are a handful per field on a payload
measured in thousands.
The third limit is that names are load-bearing in two directions. The model reads them, and so does every consumer of the response: the deserialisation target, the database writer, the dashboard query, the four services that already parse this payload. Choosing names for the model can therefore look like a migration, which is the usual reason a bad name survives three quarters after someone noticed it. That tension is real and it is resolvable — the schema you hand the model does not have to be the schema you persist, and chapter 6, on nesting depth, makes the case for treating them as separate artifacts with a mapping between them — but it is worth naming here, because "we can't rename it, it's in the API" is the objection this chapter will actually meet.
A name is one or two tokens of instruction delivered at the only moment that is guaranteed to matter, and that is both why it is worth spending an afternoon on and why it cannot carry much. Put the unit in it when the unit is in doubt, and prefer a name the source document can satisfy over a name the business would prefer to have answered, because a field that cannot be filled honestly will be filled anyway.
What the name cannot carry, something else has to. In the procurement example the
unit moved into the name and the field became discount_fraction, but the
questions that remain — what to emit when the document states a discount in
dollars rather than a percentage, which figure wins when the header and the line
items disagree, what absence looks like — all need sentences. Those sentences go
in the description, which is not documentation sitting beside the schema for a
human's benefit. It travels to the model, it is applied at the moment the field
it sits on is being generated, and that gives a rule about where every
field-level instruction in your system belongs.
Schema design for LLMs — read the current version at https://daviddominguez.dev/learn/en/schema-design-for-llms/naming-a-schema-field