Skip to content

LangChain & LangGraph

weirding produces ordinary Pydantic v2 models, so it composes with LangChain and LangGraph with no adapter. You author your schema once in XML, compile it to a model, and hand that model to LangChain's structured-output and state APIs exactly as you would a hand-written BaseModel. Claude is one of many chat models this works with — the same code runs against any LangChain BaseChatModel that supports with_structured_output.

Structured output

define_model(...) returns a type[BaseModel]. Pass it straight to with_structured_output — no wrapping required.

import weirding
from langchain_anthropic import ChatAnthropic  # or any BaseChatModel

SCHEMA_XML = """
<Extraction description="Named entities extracted from a document">
  <entity type="string" required="true" description="The named entity"/>
  <category type="string" required="true" enum="PERSON|ORG|LOC"
            description="Entity category"/>
  <confidence type="number" required="true" minimum="0" maximum="1"/>
</Extraction>
"""

Extraction = weirding.define_model(SCHEMA_XML)

llm = ChatAnthropic(model="claude-sonnet-4-5")
structured = llm.with_structured_output(Extraction)

result = structured.invoke("Acme Corp announced a partnership with Jane Doe in Berlin.")
# result is an Extraction instance

Tool descriptions now populate

LangChain derives the tool/function description it sends to the provider from the model's __doc__. As of weirding's Phase 2 change, a generated model carries the schema's top-level description attribute into Model.__doc__, so the example above sends "Named entities extracted from a document" as the tool description instead of an empty string. Always set a description on the root element — it materially improves tool-calling accuracy.

LangGraph Pydantic state

A weirding model works as a LangGraph state channel or as a typed field inside a larger state schema, because LangGraph serializes state through Pydantic.

from typing import TypedDict
from langgraph.graph import StateGraph, END

class GraphState(TypedDict):
    text: str
    extraction: Extraction  # weirding-generated model as a typed state field

def extract_node(state: GraphState) -> dict:
    out = structured.invoke(state["text"])
    return {"extraction": out}

builder = StateGraph(GraphState)
builder.add_node("extract", extract_node)
builder.set_entry_point("extract")
builder.add_edge("extract", END)
graph = builder.compile()

final = graph.invoke({"text": "Acme Corp partnered with Jane Doe in Berlin."})

Gotchas

OpenAI strict mode

When the underlying model is OpenAI's, with_structured_output defaults to the json_schema strict path, which rejects schemas that are not fully strict-compliant (every object needs additionalProperties: false and all-required, and several keywords are disallowed). If a weirding model trips this, use the function-calling escape hatch:

structured = llm.with_structured_output(Extraction, method="function_calling")

For a first-class strict-mode schema, export it explicitly with weirding.to_json_schema(weirding.compile(SCHEMA_XML), strict=True) and supply it through the provider's native response_format (see the OpenAI & Azure guide). Note that strict mode is lossy — it drops constraints such as minimum/maximum/pattern.

Nested $ref with Ollama / Llama-family runtimes

Some open-weight runtimes (Ollama, Llama.cpp grammars) handle nested $ref/$defs unevenly. weirding's native-annotation compiler inlines nested objects and emits no $ref, so models from define_model(...) are safe. If you build the IR yourself or use a dialect that emits $ref, export with weirding.to_json_schema(ir, strict=True), which inlines all local refs. See the open-weight guide.

LangGraph checkpointing and the dynamic class

weirding builds models dynamically (via type(...)), so the class object is not picklable by qualified name across a process boundary. State instances serialize fine (LangGraph dumps them as field data), so single-process checkpointing works. If you need cross-process class transport — distributed workers that must reconstruct the type by import path — define the model at module scope (so it has a stable import path) or rebuild it inside the graph-build step on each worker. Do not pass the class itself through a checkpointer.

# module scope — stable import path for cross-process workers
Extraction = weirding.define_model(SCHEMA_XML)