Skip to content
NLEN
Illustration: Structured Output via Regex and Grammar Constraints

Structured Output via Regex and Grammar Constraints

By Ivo Donker — compiled with AI assistance (Claude & Gemini)

Generating structured data such as JSON, YAML, or strict enum values via a Large Language Model (LLM) is notorious in production environments for unpredictable syntax errors. Anyone who relies solely on natural-language instructions in the prompt quickly finds that a missing brace, an escaped quote sequence, or unexpected Markdown annotations immediately crash downstream parsers. For that reason, the field has evolved from defensive prompt engineering toward formal restrictions at the decoder level. In this article we look at how constrained decoding works via regular expressions (regex), context-free grammars (CFG), and deterministic finite automata (DFA).

The central principle of this approach is simple: instead of hoping the neural network sticks to the syntax rules, we enforce, at every decoding step, that the model can only select tokens that are valid according to a predefined grammar. Anyone who first wants to dive into the broader context of format control can turn to the overview article on how to get an LLM to reliably respond in a fixed output format which outlines the differences between instructions, post-processing, and inference-time restrictions.

The anatomy of LLM decoding and logit masking

To understand how grammar restrictions work, we need to look at the final layer of an autoregressive language model. After the transformer layers, the network produces, for every token position, a vector of unnormalized log-probabilities, known as logits. The length of this vector equals the size of the tokenizer's vocabulary ($V$). Under normal circumstances, a softmax operation transforms these logits into a probability distribution, after which sampling (via parameters such as temperature or top-p) selects the next token.

With constrained decoding, we intervene on the logit vector before the softmax computation takes place. If we know at position $t$ that only digits are allowed, we mask all tokens in the vocabulary that contain letters or punctuation by manually setting their logit value to negative infinity ($-\infty$). After the softmax operation, these invalid tokens have a probability of exactly $0\%$, making it mathematically impossible for the sampler to select them.

Formal definition of logit masking:
Let $z \in \mathbb{R}^{|V|}$ be the logit vector and $M \subseteq \{1, \dots, |V|\}$ the set of valid token IDs at step $t$. The masked logit $\tilde{z}_i$ is computed as:

$\tilde{z}_i = z_i \text{ if } i \in M, \text{ else } -\infty$.

The real complexity lies not in the math of the masking, but in efficiently determining the valid set $M$ at every step $t$ without inference latency exploding. This requires state machines that run in sync with the generated byte streams.

Regular expressions and deterministic automata (DFA)

When the desired output can be described with a regular expression — such as a date format \d{4}-\d{2}-\d{2}, an IPv4 address, or a fixed set of categories — modern inference engines convert this expression in advance into a Deterministic Finite Automaton (DFA). A DFA consists of a finite set of states, a start state, transition functions based on characters (or bytes), and accepting states.

Because LLMs don't operate at the level of individual characters but on subword tokens (which can be multiple bytes long), the DFA has to be projected onto the token vocabulary. For every token in the vocabulary, it's evaluated whether the sequence of bytes within that specific token forms a valid path from the current DFA state to a next state. Only tokens whose complete byte sequence enables a valid transition are included in mask $M$.

// Voorbeeld van een eenvoudige DFA transitie-evaluatie in pseudo-code
function getValidTokens(currentState, dfa, vocabulary):
  validTokens = []
  for token in vocabulary:
    state = currentState
    isValid = true
    for byte in token.bytes:
      if dfa.hasTransition(state, byte):
        state = dfa.nextState(state, byte)
      else:
        isValid = false
        break
    if isValid:
      validTokens.append(token.id)
  return validTokens

Libraries such as Outlines drastically optimize this process by precomputing and caching the entire token transition table offline (before inference). This means looking up valid tokens per decoding step costs only a constant-time lookup ($O(1)$), resulting in negligible latency overhead during streaming.

Context-free grammars (CFG) and GBNF

For hierarchical structures with nested elements — such as arbitrary JSON objects, arrays within arrays, or programming languages — regular expressions fall short because they lack a memory stack. This is where Context-Free Grammars (CFG) come in. A common standard in open-source engines such as llama.cpp is GBNF (GGML BNF), a derivative of Backus-Naur Form.

A GBNF grammar defines production rules that specify which sequences of non-terminal and terminal symbols are allowed. During decoding, a pushdown automaton (a state machine with a stack) keeps track of which parsing context the generator is in. Below is an example of a GBNF grammar that enforces a strict JSON object with a status field and a list of numeric IDs.

# GBNF Grammatica voor een strict taak-status JSON object
root   ::= "{" ws "\"taak_id\":" ws id "," ws "\"status\":" ws status "," ws "\"logs\":" ws log_array "}"
id     ::= [0-9]+
status ::= "\"in_behandeling\"" | "\"voltooid\"" | "\"gefaald\""
log_array ::= "[" ws (log_item ("," ws log_item)*)? ws "]"
log_item  ::= "\"" [^"\\]* "\""
ws     ::= [ \t\n\r]*

During every iteration, the parser evaluates which bytes are grammatically allowed as the next input. Tokens that don't correspond to a valid reduction or shift within the grammar are masked immediately. This guarantees that the final string is guaranteed to parse with standard parsers.

Mapping JSON Schema to grammars

In modern software architectures, developers rarely write raw GBNF or EBNF grammars by hand. Instead, a JSON Schema (often generated from data models such as Pydantic or Zod) forms the source artifact. Specialized compilers automatically translate this schema into an underlying state machine or grammar.

JSON Schema Construct Grammar Equivalent DFA / Parser Behavior
"type": "string", "enum": ["a", "b"] Exact alternatives: ("\"a\"" | "\"b\"") Direct literal transitions; masks all other characters.
"type": "integer", "minimum": 10 Regex subset: [1-9][0-9]+ Only allows digit sequences that don't start with 0.
"type": "array", "items": {...} Recursive rule with separator Manages a stack with commas between elements and closes with ].
"additionalProperties": false Strict field sequence or permutations Blocks the opening of invalid JSON keys.

Anyone who wants to compare the practical implementation of this via commercial APIs with open-source engines can consult the documentation on getting reliable JSON and structured output from LLMs, which details specific API parameters such as response_format in depth.

The interplay between constraints and model intelligence

Although grammar restrictions guarantee 100% syntactic correctness, they introduce a subtle but crucial risk to the model's semantic quality and reasoning power. This phenomenon is known as the token prefix sampling dilemma.

A neural network generates its response based on prior autoregressive correlations. When a grammar forces the model to pick a specific token that scores very low in the unfiltered probability distribution, the internal attention representation (attention state) gets disrupted. If, for example, we force a model to open directly with {"analyse": "..."} without allowing it to first generate intermediate steps, its capacity for logical reasoning gets curtailed.

Visualization of logit masking, where invalid tokens are filtered out relative to a grammar tree
Logit masking: tokens that fall outside the grammatical paths are eliminated from the sampling space.

To counter this quality degradation, advanced implementations combine reasoning space (such as a "thought" or "reasoning" field) before the formal answer. This allows the model to reason freely within a string field before the grammar switches to strict enums or numeric structures.

Overview of frameworks and tools

The ecosystem around structured decoding has matured quickly. Various open-source tools and serving engines offer built-in mechanisms for logit masking:

When choosing an architecture, developers must weigh whether the logit restrictions are handled locally in the inference loop, or whether an upstream gateway should guard the format. A deeper comparison between prompt rules and API-driven schemas can be found in the guide on enforcing output via prompt rules or API schema, which helps determine when light instructions suffice and when hard grammar masks are necessary.

Practical example: JSON schema via Outlines in Python

Below is a concrete, reproducible example in which a Pydantic model is used, via Outlines, to guarantee strict JSON output from an open-source model. In the background, the generator automatically masks all tokens that deviate from the schema.

from enum import Enum
from pydantic import BaseModel, Field
import outlines

# 1. Definieer het gewenste datamodel
class RisicoNiveau(str, Enum):
    LAAG = "laag"
    GEMIDDELD = "gemiddeld"
    HOOG = "hoog"

class KlantEvaluatie(BaseModel):
    klant_id: int = Field(description="Numeriek ID van de klant")
    risico: RisicoNiveau
    score: float = Field(ge=0.0, le=100.0, description="Score tussen 0 en 100")
    samenvatting: str = Field(max_length=150)

# 2. Laad het model en compileer de gestructureerde generator
model = outlines.models.transformers("meta-llama/Llama-3.1-8B-Instruct")
generator = outlines.generate.json(model, KlantEvaluatie)

# 3. Voer de inferentie uit; de output is gegarandeerd geldig volgens het schema
prompt = "Evalueer het dossier van klant 48192 met 3 te late betalingen."
resultaat: KlantEvaluatie = generator(prompt)

print(resultaat.model_dump_json(indent=2))

In this script, the model can under no circumstances produce a value for risico that deviates from the defined enum. As soon as the quote after "risico": " is generated, the logit mask immediately restricts the choices to only the tokens for laag, gemiddeld or hoog.

Performance, overhead, and failure modes

Although grammar restrictions eliminate syntax errors, they bring specific computational and logical trade-offs that need to be monitored:

Aspect Impact of grammar decoding Mitigation strategy
Initialization phase (cold start) Compiling complex regex/CFG into a DFA can take 100ms to several seconds. Pre-compile schemas during server startup and reuse the index.
Inter-token latency Slight increase per token when dynamic parsing is needed for deeply nested trees. Use indexed bitmasks (as in xGrammar) in GPU memory.
Infinite loops (loop traps) Model gets stuck in a grammatically valid but infinite repetition of fields. Set strict max_tokens limits and array length restrictions.
Logical hallucination Forced syntax masks nonsensical answers (the model outputs syntactically valid nonsense). Combine schema validation with factual evaluation and source verification.

Should an engine still derail due to extreme context lengths or missing schema support, a safety net at the application level remains essential. For concrete recovery patterns for runtime crashes, see the article on recovery prompts for failed validation of LLM output, which explains how parsing errors are automatically fed back to the model.

Conclusion and best practices

Constrained decoding via regex and grammars transforms LLMs from unpredictable text generators into reliable software components. By mathematically bounding the valid token space at every step, syntax errors disappear entirely from the application pipeline. The technique does require thoughtful design, though: give the model enough semantic breathing room to reason first, avoid needlessly complex schemas that drive up compile time, and cache state machines wherever possible. With frameworks such as Outlines and native support in vLLM and llama.cpp, structured decoding has become a standard building block in modern AI architectures.