Skip to content
Chapter 1 of 7 · Schema design for LLMsNaming a schema field

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:

json
{
  "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 nameEmitted 0.15Emitted 15Emitted 0
discount_pct967133
discount_percent411527
discount_fraction18839

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:

json
{
  "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:

NameWhat it commits the model toWhat it leaves openAnswerable from the document
discount_pctthat a discount number existsthe unit, and what absence looks likepartly
discount_fractiona value in 0–1what absence looks likeyes
is_exploitablea verdict about a deployed systemnothing — both values are claimsno
report_claims_exploitablea reading of the report's textnothing materialyes

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

Back to top