Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Designs JSON Schemas for LLM tool/function calling that survive across providers. Use when writing tool definitions, function-calling `parameters`, an MCP tool `inputSchema`, or zod/Pydantic tool schemas; when deciding between anyOf/oneOf/allOf; when choosing format/enum/description usage; or when one schema must run on several providers or strict modes (OpenAI, Anthropic, Gemini, Vertex, Bedrock, Cohere, xAI, DeepSeek, Fireworks, FriendliAI, vLLM/SGLang, and more). Covers the canonical-schema +
.claude/skills/minpeter-tool-schema-design/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-09 | ✗→✓ | ▲ Improved | 56% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 93% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-04 | ✓→✓ | = Same ✓ | 107% | 0% |
House rules for writing LLM tool/function-calling schemas that work everywhere. Verified against the primary docs of 12+ providers and inference stacks — see references/provider-matrix.md for the per-provider evidence table.
No single JSON Schema is accepted unchanged by every provider. The decisive contradiction: additionalProperties: false is required by OpenAI strict, DeepSeek strict, and Bedrock, but stripped or rejected on Gemini/Firebase paths. Nullability spelling also differs per provider. So:
Keep one canonical schema (rich in meaning) → compile through thin per-provider adapters → validate the post-transform wire schema.
An adapter-free canonical schema is the best outcome; adapters exist only for strict-mode transforms. Never ship a lowest-common-denominator schema as the product — it is a non-strict fallback for discovery phases only.
Violating these cannot be repaired by an adapter.
| # | Rule | Why | |---|------|-----| | H1 | Root is a non-empty type: "object". No root union/enum/const. | MCP, OpenAI strict, Bedrock, Cohere, Kimi, Qwen, xAI all force an object root. Top-level enums only exist in Gemini structured output — never tool input. | | H2 | Concrete types only: object/string/number/integer/boolean/array; arrays carry one schema-valued items. | Universally supported; type errors are a graded failure class in tool-calling benchmarks. | | H3 | description on the tool and on every non-obvious property (units, constraints, selection criteria). Skip tautologies. | Provider guidance calls descriptions the largest factor in tool performance. Descriptions cost tokens and are an attack surface on untrusted tools. | | H4 | oneOf/allOf/not forbidden. Nested anyOf is allowed. | anyOf confirmed on OpenAI strict, Anthropic, xAI, DeepSeek strict, Cohere, current Gemini, Fireworks (full 2020-12), FriendliAI (anyOf only), vLLM/SGLang (XGrammar). oneOf/allOf are rejected or undocumented on several of these. Root anyOf stays banned via H1. Avoid even anyOf when targeting undocumented providers (Together, GMI, Groq, GLM/Kimi/Qwen) — split into separate tools or a string discriminator enum. | | H5 | No title. | Absent from the Firebase vocabulary; costs nothing to skip. | | H6 | Limits: nesting ≤ 10, total fields ≤ 200, tools ≤ 20 per request. | OpenAI depth cap 10; Cohere counts an aggregate 200-field budget; Anthropic strict allows 20 tools/request. Accuracy degrades as tool count grows. | | H7 | No $ref/$defs/default/const/numeric-length constraints/pattern in the canonical schema. | An adapter-free schema beats a clever one. Inline repeated structures; put constraints in the description prose and validate server-side. |
| # | Rule | Adapter behavior | |---|------|------------------| | S1 | required reflects real optionality. Optional fields are fine. | OpenAI/DeepSeek strict: make every property required and encode optional as a nullable union. Everyone else: pass through. | | S2 | Closed-object intent. | Inject additionalProperties: false recursively for OpenAI/DeepSeek/Bedrock; omit for Gemini/Firebase paths; omit for xAI (already default false). | | S3 | Nullability is intent only. | OpenAI: null union / Anthropic: ["T","null"] / Gemini: nullable: true / xAI & loose Anthropic: omit from required. Never conflate missing with null. Note: required+nullable has recorded runtime failures in production SDK paths — validate returned arguments. | | S4 | Enums: string-valued on properties; use them for genuinely closed sets. | Legacy Gemini typed Schema only allows string enums (repeated string); numeric enums need the parametersJsonSchema path or string conversion. Under strict decoding enums are masked at token level — the strongest guarantee available. High-cardinality or evolving vocabularies belong in a string + lookup tool instead. | | S5 | Use format (date, date-time, email, uuid, …). | Enforced by OpenAI strict, xAI, DeepSeek (5 formats); ignored but never rejected elsewhere. Never rely on it for validation — re-validate at the application boundary and repeat the semantics in the description ("ISO 8601 with UTC offset"). | | S6 | ~~$ref/default/constraints tolerated~~ — superseded by H7: forbidden in the canonical schema. | Removing the keyword class removes the adapter logic for it entirely. |
additionalProperties: false + all properties required + optional → nullable union. Always set strict: true.minLength/maxLength/minItems/maxItems; only email/hostname/ipv4/ipv6/uuid formats; beta base URL.additionalProperties: false. Cap tools at 128 (docs contradict: 200 guide vs 128 API ref).input_examples available; ≤ 20 strict tools.additionalProperties/strict wrappers, null union → nullable: true, inline $defs, drop title/default. parameters (OpenAPI subset) and parametersJsonSchema (JSON Schema) are different dialects — pick one and verify.additionalProperties: false; Draft 2020-12 subset only.strict_tools for no-arg tools); 200-field aggregate budget; remove oneOf/allOf.oneOf.parameters, required, and additionalProperties in production. On OpenRouter set require_parameters: true.^[a-zA-Z0-9_-]{1,64}$ (the OpenAI∩Anthropic intersection; Gemini's wider charset is not portable). Namespace by service/resource (asana_projects_search); prefix vs suffix ordering measurably changes selection — choose by eval.action enum parameter, not one tool per endpoint.input_examples (schema-validated, ~20–200 tokens); OpenAI warns examples can hurt reasoning models — put them in instructions instead.concise|detailed detail parameter; paginate/truncate with defaults; on MCP emit outputSchema + structuredContent + a text mirror.Structure gets a schema accepted; authoring determines whether the model calls it correctly. Full guide with templates: references/authoring.md. The short version:
verb_noun (get_user_profile, neverfetch(id)); service-prefix when catalogs overlap.
→ when NOT to use + the alternative tool → what it returns → caveats. 3–4 sentences; more only for complex tools. The "do NOT use for X, use Y" line is the highest-value sentence when tools overlap.
docstring-speak. Apply the intern test — a new engineer given only the schema should call the tool correctly.
("IANA time zone, e.g. America/Los_Angeles"). Defaults go in prose ("default 500, max 2000"), never the default keyword (H7). Keep required minimal — models hallucinate values for required params the user never mentioned.
{success: false, error, retry_hint} with asmall canonical code set (VALIDATION_ERROR, RATE_LIMITED, …) and a retryable flag. The hint names the concrete fix.
behavior — re-run tool evals after every edit.
accepts → validates schema → constrains decoding → guarantees call → guarantees args. A non-strict enum is a hint, not a guarantee.Deeper production knowledge — failure taxonomy, strict-mode costs, tool-count pressure, schema–model misalignment, schema compilation — with verification labels: references/deep-dive.md.
GLM/Kimi/Qwen keyword-level acceptance (no public matrix) · xAI tool cap (200 vs 128 docs contradiction) · OpenAI/Anthropic oneOf status (undocumented) · schema-size↔accuracy curves (no controlled studies).
Nitpick: zod v4's z.toJSONSchema() always emits a root $schema key with no opt-out; no provider is known to reject it — ignore unless a rejection is observed.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 41,135 | 29,822 | -28% | 1 | 1 | 0% | 6,401 | 7,123 | +11% | 0 | 0 | — |
case-02 | fail→fail | 24,621 | 26,019 | +6% | 1 | 1 | 0% | 4,494 | 7,287 | +62% | 0 | 0 | — |
case-03 | fail→fail | 19,285 | 22,159 | +15% | 1 | 1 | 0% | 3,451 | 6,843 | +98% | 0 | 0 | — |
case-04 | pass→pass | 13,405 | 10,720 | -20% | 1 | 1 | 0% | 2,123 | 4,384 | +107% | 0 | 0 | — |
case-05 | pass→pass | 18,995 | 14,686 | -23% | 1 | 1 | 0% | 3,220 | 5,498 | +71% | 0 | 0 | — |
case-06 | pass→pass | 13,997 | 9,589 | -31% | 1 | 1 | 0% | 2,217 | 3,995 | +80% | 0 | 0 | — |
case-07 | fail→fail | 16,258 | 18,053 | +11% | 1 | 1 | 0% | 2,384 | 5,573 | +134% | 0 | 0 | — |
case-08 | pass→pass | 11,471 | 8,482 | -26% | 1 | 1 | 0% | 1,751 | 3,870 | +121% | 0 | 0 | — |
case-09 | fail→pass | 26,662 | 20,326 | -24% | 1 | 1 | 0% | 3,677 | 5,718 | +56% | 0 | 0 | — |
case-10 | fail→pass | 13,022 | 11,062 | -15% | 1 | 1 | 0% | 2,343 | 4,522 | +93% | 0 | 0 | — |
case-11 | pass→pass | 14,565 | 16,200 | +11% | 1 | 1 | 0% | 2,431 | 4,945 | +103% | 0 | 0 | — |
case-12 | pass→pass | 11,370 | 10,641 | -6% | 1 | 1 | 0% | 1,926 | 4,390 | +128% | 0 | 0 | — |
case-13 | pass→pass | 15,833 | 9,972 | -37% | 1 | 1 | 0% | 2,210 | 4,412 | +100% | 0 | 0 | — |
case-14 | fail→pass | 17,419 | 16,070 | -8% | 1 | 1 | 0% | 2,865 | 5,229 | +83% | 0 | 0 | — |
case-15 | fail→fail | 13,208 | 18,764 | +42% | 1 | 1 | 0% | 2,096 | 5,805 | +177% | 0 | 0 | — |
case-16 | pass→pass | 13,059 | 10,960 | -16% | 1 | 1 | 0% | 2,096 | 4,642 | +121% | 0 | 0 | — |
case-17 | pass→pass | 16,625 | 18,695 | +12% | 1 | 1 | 0% | 2,846 | 5,772 | +103% | 0 | 0 | — |
case-18 | pass→pass | 17,831 | 19,208 | +8% | 1 | 1 | 0% | 2,748 | 5,491 | +100% | 0 | 0 | — |
case-19 | pass→pass | 23,043 | 22,005 | -5% | 1 | 1 | 0% | 4,445 | 5,987 | +35% | 0 | 0 | — |
case-20 | pass→pass | 15,238 | 18,051 | +18% | 1 | 1 | 0% | 2,624 | 5,586 | +113% | 0 | 0 | — |
case-21 | pass→pass | 17,924 | 16,126 | -10% | 1 | 1 | 0% | 2,449 | 5,185 | +112% | 0 | 0 | — |
case-22 | fail→pass | 12,762 | 5,979 | -53% | 1 | 1 | 0% | 2,173 | 3,577 | +65% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +18 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.