Skip to content

fix: ensure empty object schemas include required field for OpenAI compatibility#1702

Open
owendevereaux wants to merge 1 commit intomodelcontextprotocol:mainfrom
owendevereaux:fix/empty-object-schema-required
Open

fix: ensure empty object schemas include required field for OpenAI compatibility#1702
owendevereaux wants to merge 1 commit intomodelcontextprotocol:mainfrom
owendevereaux:fix/empty-object-schema-required

Conversation

@owendevereaux
Copy link

Summary

When generating JSON Schema from Zod schemas via schemaToJson(), object schemas now always include the required field, even when empty. This is necessary for compatibility with OpenAI's strict JSON schema mode, which mandates that required always be present.

Problem

As reported in #1659, when registering a tool using an empty Zod object as inputSchema:

server.registerTool(
  "example-tool",
  {
    description: "Example tool without input",
    inputSchema: z.object({}).strict(),
  },
  async () => ({ success: true })
);

The generated JSON schema is:

{
  "type": "object",
  "properties": {},
  "additionalProperties": false
}

This is valid JSON Schema, but OpenAI's strict mode requires required to always be present:

Schema validation failed
The schema has structural issues:
root: Schema must have the following keys: required

Solution

Modified schemaToJson() to post-process the JSON Schema output and ensure all object schemas have a required field. The fix recursively processes:

  • Direct object schemas
  • Nested object schemas in properties
  • Array item schemas
  • additionalProperties schemas
  • allOf/anyOf/oneOf combinators
  • Schema references in $defs

Now the output is:

{
  "type": "object",
  "properties": {},
  "additionalProperties": false,
  "required": []
}

Testing

Added comprehensive tests covering:

  • Empty object schemas
  • Objects with only optional properties
  • Objects with required properties (unchanged behavior)
  • Nested object schemas
  • Array item schemas
  • Deeply nested structures

All existing tests pass.

Fixes #1659

…mpatibility

When generating JSON Schema from Zod schemas via schemaToJson(), object
schemas now always include the 'required' field, even when empty. This
is necessary for compatibility with OpenAI's strict JSON schema mode,
which mandates that 'required' always be present.

The fix recursively processes all nested object schemas, including those
in properties, array items, additionalProperties, allOf/anyOf/oneOf, and
$defs.

Fixes modelcontextprotocol#1659.
@owendevereaux owendevereaux requested a review from a team as a code owner March 18, 2026 22:04
@changeset-bot
Copy link

changeset-bot bot commented Mar 18, 2026

🦋 Changeset detected

Latest commit: 788a86a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@modelcontextprotocol/core Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link

@travisbreaks travisbreaks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good fix for the OpenAI strict mode compatibility issue. The recursive approach is thorough. A few observations:

1. In-place mutation
ensureRequiredField mutates the schema object directly. If z.toJSONSchema() caches or reuses objects internally, this could cause surprising side effects. Safer to spread into a new object:

const result = { ...schema };
if (result.type === 'object' && !('required' in result)) {
    result.required = [];
}

2. Missing recursive cases
The function handles properties, additionalProperties, items, allOf/anyOf/oneOf, and $defs, but misses:

  • not (e.g. z.object({}).not(...))
  • if / then / else (conditional schemas)
  • prefixItems (tuple schemas in JSON Schema 2020-12)

These are less common with Zod output, but if the goal is general JSON Schema compliance for OpenAI, they could appear in user-constructed schemas.

3. Test coverage gap
Tests cover nested objects, arrays, and deep nesting, but none of the combiner paths (allOf, anyOf, oneOf) are tested. A test with z.union([z.object({}), z.object({})]) would validate that branch.

Overall a clean contribution. The core logic is correct and the tests cover the primary use case well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

inputSchema generated from empty Zod object is incompatible with OpenAI strict JSON schema mode

3 participants