The one-line version: vLLM took its five loose structured-decoding request fields — guided_json, guided_regex, guided_choice, guided_grammar, guided_whitespace_pattern — and folded them into a single structured_outputs object. The old fields still parse for now, but they log a deprecation and are on the way out. The fix is mechanical, and this is the copy-paste version.

If you serve open models on your own GPUs, structured decoding is how you stop a model from returning prose when you asked for JSON. vLLM has had it for a long time under the guided_* names, and every tutorial written before mid-2026 uses them. Those tutorials are now wrong. Here is what changed and exactly what to type instead.

What actually changed#

Three things, and only three:

  1. **The guided_* request fields moved under one object.** Every constraint now lives inside structured_outputs, keyed by the suffix of the old field name. guided_jsonstructured_outputs={"json": ...}, and so on down the list.
  2. guided_decoding_backend was removed from the request. You no longer choose the backend per call. It is a server-launch flag now.
  3. Everything else is the same. The values — your JSON Schema, your regex, your list of choices, your EBNF grammar — do not change. Only the envelope does.

That is the whole migration. The rest of this piece is the exact syntax for each surface you might be calling from.

Migrating the server (OpenAI-compatible endpoint)#

The OpenAI Python SDK has no structured_outputs argument, so you tunnel it through extra_body. Before:

# OLD — deprecated
completion = client.chat.completions.create(
    model=model,
    messages=[{"role": "user", "content": "Classify the sentiment."}],
    extra_body={"guided_choice": ["positive", "negative", "neutral"]},
)

After:

# NEW
completion = client.chat.completions.create(
    model=model,
    messages=[{"role": "user", "content": "Classify the sentiment."}],
    extra_body={"structured_outputs": {"choice": ["positive", "negative", "neutral"]}},
)

The same nesting applies to a JSON Schema:

extra_body={"structured_outputs": {"json": my_json_schema}}

If all you need is JSON-schema-shaped output, there is a more portable path that needs no extra_body at all — the standard response_format, which works the same way across most inference servers and hosted APIs:

response_format={
    "type": "json_schema",
    "json_schema": {"name": "sentiment", "schema": my_json_schema},
}

Prefer response_format when you might swap backends; reach for structured_outputs when you need vLLM-specific constraints like regex, choice, or a raw EBNF grammar. (If you are still deciding which constraint style to use in the first place, we walked through JSON mode vs function calling vs constrained decoding separately.)

Migrating offline inference (the LLM class)#

Batch and offline callers build a params object instead of sending JSON. Import StructuredOutputsParams and hang it off SamplingParams:

from vllm import LLM, SamplingParams
from vllm.sampling_params import StructuredOutputsParams

llm = LLM(model="your-model")

sampling_params = SamplingParams(
    structured_outputs=StructuredOutputsParams(choice=["Positive", "Negative"]),
)
out = llm.generate(prompts="Classify: 'the flight was on time'", sampling_params=sampling_params)

The fields inside StructuredOutputsParams are the same set as the server object — json, regex, choice, grammar, structural_tag — so once you know the server mapping, the offline one is free.

The backend flag: where guided_decoding_backend went#

Backend selection left the request entirely. You set it once, at launch:

vllm serve your-model \
    --structured-outputs-config.backend auto

auto is the default and the right answer for almost everyone: vLLM inspects each request and picks a backend from the constraint type. The two named backends behind it are xgrammar (the fast default, built for near-zero-overhead JSON) and guidance (the llguidance engine, which is more permissive with awkward grammars). Pin one only when you have measured a reason — a grammar xgrammar rejects, or a latency delta you can actually see. We compared the engines themselves in Outlines vs XGrammar vs llguidance; the short version is that auto already routes around most of the sharp edges.

One trap: reasoning models#

If you constrain a reasoning model naively, the grammar clamps the thinking tokens too and the output degrades. vLLM has an opt-in for this. Launch with the model's reasoning parser and enable structured outputs inside reasoning:

vllm serve your-reasoning-model \
    --reasoning-parser deepseek_r1 \
    --structured-outputs-config.enable_in_reasoning=True

Now the free-form reasoning comes back on completion.choices[0].message.reasoning and only the final answer in content is constrained. This is the one place the migration is more than a rename — if you serve a reasoning model behind a schema, you need this flag or you will quietly lose quality. (Worth remembering that constraints are not free even when wired correctly: we covered whether structured output hurts accuracy and it sometimes does.)

Do it now#

The loose guided_* fields still work today, which is exactly why this is easy to ignore — until a vLLM bump stops accepting them and a batch job fails at 2 a.m. The change is a find-and-replace: nest each field, delete guided_decoding_backend, and move the backend to your launch command. Twenty minutes now beats a broken pipeline later, and the new surface is genuinely cleaner — one object, one place, one flag.