Skip to content

fix(langgraph): keep JSON Schema semantics for object values and validate node I/O - #256

Open
fede-kamel wants to merge 1 commit into
oracle:mainfrom
fede-kamel:fix/langgraph-schema-fidelity
Open

fix(langgraph): keep JSON Schema semantics for object values and validate node I/O#256
fede-kamel wants to merge 1 commit into
oracle:mainfrom
fede-kamel:fix/langgraph-schema-fidelity

Conversation

@fede-kamel

Copy link
Copy Markdown
Member

Fixes #220, #231, #233, #239, #251, #252. Fixes the LangGraph part of #241 (the Wayflow part lives in wayflowcore).

Root cause

Values flowing through the LangGraph adapter lost the JSON Schema semantics of their Agent Spec properties, and nothing checked them against the declared schemas:

Changes

  • adapters/_utils.py: generated object models derive from a new AgentSpecObjectModel. Bare object schemas and typed dictionaries map to Dict[str, ...]; extra keys are kept unless additionalProperties is false (JSON Schema default); declared defaults are applied; to_json_value() converts models back to plain JSON, omitting optional fields that were neither provided nor defaulted. New helpers apply_json_schema_defaults and get_json_schema_validation_errors (uses the jsonschema package that is already a core dependency).
  • langgraph/_langgraphconverter.py: tool callables (server, remote, client, and BaseTool func/coroutine) are wrapped with _with_json_arguments so they receive JSON values; user-provided StructuredTools with their own args_schema are untouched (to_json_value only converts models generated from Agent Spec schemas).
  • langgraph/_node_execution.py: node inputs and outputs are converted to JSON values, nested defaults are applied, and each value is validated against its property's JSON schema with an error naming the node, the property and every violation. Structured agent outputs go through to_json_value. The zero-output ToolNode branch short-circuits.
  • Tests: tests/adapters/test_schema_models.py (unit, no runtime needed), tests/adapters/langgraph/flows/test_schema_fidelity.py (the scenarios of the issues end to end, plus the zero-output ToolNode), tests/adapters/langgraph/test_tool_json_arguments.py (sync, async and client-tool interrupt payloads). One existing test declared an object output but expected the scalar 1 to come out of it; it now declares an untyped output.
  • Changelog entry under Bug fixes.

Behaviour changes to be aware of

  • Flows that used to run with values not conforming to their declared schemas now fail at the node boundary with an explicit error. This matches Wayflow and the spec's intent; lenient casts (numeric strings to numbers, anything to string) are kept.
  • Objects whose schema does not mention additionalProperties now keep extra keys instead of silently dropping them (JSON Schema default).
  • The CrewAI adapter shares create_pydantic_model_from_properties, so its nested object models gain the same defaults/extras behaviour. Its tests were not run (CrewAI is not installable alongside the other adapters).

Verification

  • New tests: 35 passed. Full suite with the LangGraph, AutoGen, Agent Framework, OpenAI Agents and evaluation extras: 1214 passed, 586 skipped (SKIP_LLM_TESTS=1), up from 1179 on main.
  • Public CI steps (black, isort, flake8 + copyright, bandit, mypy, tests/run_tests.sh with core dependencies only) reproduced locally on Python 3.10 through 3.14.
  • The same flows were run on wayflowcore 26.3.0 for comparison: outputs and rejections now match across the two runtimes.

Notes for reviewers

…date node I/O

The pydantic models generated from Agent Spec object schemas lost the
semantics of the schemas they came from, and node executors never
checked values against the declared schemas:

- a bare object schema (or a typed dictionary) became an empty model
  that silently stripped every key (oracle#220);
- additional properties were dropped even when the schema allowed them
  (oracle#239);
- declared defaults of omitted nested properties were replaced by None
  (oracle#241, LangGraph part);
- tools received pydantic model instances for object arguments instead
  of the plain dictionaries used by the other runtimes, and those
  instances leaked into flow outputs and client-tool interrupt payloads;
- StartNode inputs and EndNode outputs violating their nested schema
  (missing required property, wrong type, unexpected key) were accepted
  (oracle#231, oracle#233);
- a ToolNode without declared outputs raised "Unsupported multi-output
  mapping" for any tool return value that was not a dict or tuple.

Generated object models now derive from AgentSpecObjectModel: bare
object schemas map to Dict[str, Any] (typed when additionalProperties
is a schema), extra keys are kept unless additionalProperties is false,
declared defaults are applied and unset optional fields are omitted
when converting back to JSON with to_json_value(). Tool callables are
wrapped so they receive JSON values, and structured agent outputs are
converted the same way. Node executors apply nested defaults and
validate every input/output value against its JSON schema with the
jsonschema package, reporting each violation with the node and property
names. The zero-output ToolNode branch now short-circuits the mapping.

One existing test declared an object output but expected the scalar 1
to come out of it; it now declares an untyped output.

Fixes oracle#220, oracle#231, oracle#233, oracle#239. Fixes the LangGraph part of oracle#241.

Signed-off-by: Federico Kamelhar <federico.kamelhar@oracle.com>
@fede-kamel
fede-kamel requested a review from a team September 11, 2026 16:22
@oracle-contributor-agreement oracle-contributor-agreement Bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Sep 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bare object schemas in tool parameters silently strip all keys (empty create_model + pydantic extra='ignore')

1 participant