Your DSPy field constraints never reach the model
DSPy drops Pydantic field metadata before your schema leaves the process, so no structured-output backend ever sees those constraints. Here is what to do instead.
DSPy drops Pydantic field metadata before your schema leaves the process, so no structured-output backend ever sees those constraints. Here is what to do instead.
If you write this DSPy signature:
import dspy, pydantic
from typing import Annotated
class Score(dspy.Signature):
text: str = dspy.InputField()
score: float = dspy.OutputField(ge=0.0, le=1.0, multiple_of=0.25)
you probably believe the model is constrained to emit a score between 0 and 1 in steps of 0.25. It is not. Print the schema DSPy actually derives for structured outputs:
from dspy.adapters.json_adapter import _get_structured_outputs_response_format
print(_get_structured_outputs_response_format(Score, True).model_json_schema())
# {'properties': {'score': {'title': 'Score', 'type': 'number'}}, ...}
ge, le, and multiple_of are gone. DSPy's signature-to-model derivation drops pydantic Field metadata before the schema leaves your process, so no structured-output backend ever sees those constraints, not OpenAI's, not vLLM's, not any grammar engine. Your pipeline type-checks, your outputs parse, and a score of 0.37 sails through until something downstream notices. We found this while wiring GRID into DSPy and verified it against dspy 3.2.1. It affects every backend equally.
Two practical consequences:
tags: set[str] survives derivation and becomes uniqueItems. Literal["a", "b"] survives. Nested pydantic models survive. OutputField(multiple_of=...) does not.GRID is our constrained-decoding engine (Apache-2.0). Its contract is that nothing fails silently: every constraint in a schema is either enforced by the token mask, recorded by name so you know exactly what to re-validate, or declared unsupported up front. That contract turns out to be the missing piece for typed pipelines:
pip install grid-guardrail dspy
from grid.integrations.dspy_adapter import GridJSONAdapter, assert_enforceable
adapter = GridJSONAdapter(strict=True)
dspy.configure(adapter=adapter)
class Extract(dspy.Signature):
text: str = dspy.InputField()
verdict: str = dspy.OutputField()
tags: set[str] = dspy.OutputField() # set -> uniqueItems
program = dspy.Predict(Extract)
assert_enforceable(program, adapter)
# SignatureNotEnforceable: strict: uniqueItems at $.tags
That exception fires when you build the program, not three weeks later when a duplicate tag corrupts a join. Drop strict=True and the same information arrives as data instead of an error:
adapter = GridJSONAdapter()
adapter.recorded_paths_for(Extract) # {'$.tags': {'uniqueItems'}}
recorded_paths_for is the honesty contract as an API. It returns the exact, named constraints that GRID accepted but did not mask-enforce, located on the output field they live on, so your validation code checks $.tags for uniqueness and nothing else. (recorded_for returns the same information as a flat set of names.) For most pydantic-derived signatures the set is empty, since enum and Literal fields, nested models, and required keys all sit in the easy region of JSON Schema. At that point the parse-retry machinery in your framework becomes dead weight, because typed fields cannot arrive malformed.
The same check runs repo-wide as a CI gate:
$ python -m grid.integrations.dspy_check src/pipelines.py --strict
ENFORCEABLE Summarize
RECORDED Extract [$.tags: uniqueItems]
2 signature(s): 1 enforceable, 1 with recorded residue, 0 declared unsupported
$ echo $? # --strict: nonzero unless everything is mask-enforceable
1
A teammate's set[str] fails the PR, not the pipeline three weeks later.
Running against a GRID-enabled server (our vLLM integration), one argument moves enforcement server-side:
GridJSONAdapter(mode="server") # attaches the compiled grammar per request
Client mode, the default, changes nothing about your requests and works against any OpenAI-compatible endpoint today.
The walk-away cases deserve the same clarity. If your signatures are a handful of enum-and-string shapes you already test end to end, the provider's native structured outputs are enough, and this adapter adds a dependency for an empty residue set. The adapter earns its place when signatures come from many hands, evolve weekly, and feed systems where "parsed" and "correct" are different words.
Everything above is measured and committed. The engine's full JSONSchemaBench results (11,306 real-world schemas, three engines, one machine, with per-schema statuses in the repo) are at github.com/evolutionIdGmbH/grid, including the rows where we lose.