Skip to content

[JS] zod.literal breaks schema parsing #2869

@christophe-g

Description

@christophe-g

Describe the bug
Schema containing zod.literals seem to break schema, with error message difficult to interpret

To Reproduce

For instance:

// run this through `pnpx tsx test.ts` 

import { gemini25FlashPreview0417, googleAI } from '@genkit-ai/googleai';
import { genkit, z } from 'genkit';

const optionSchema = z.object({
  locale: z.object({
    label: z.string().describe('The text/label of the option'),
    supportingText: z.string().optional().describe('The supporting text of the option'),
  }),
  description: z.string().optional().describe('The description of the option'),
}).describe('The option for a question');

// Base schema with common fields including the locale structure
const baseQuestionSchema = z.object({
  locale: z.object({
    label: z.string().describe('The text/label of the question being asked'),
    supportingText: z.string().optional().describe('The supporting text of the question'),
  }),
  items: z.array(optionSchema).optional().describe('The options for the checkbox question'),
  description: z.string().optional().describe('The description of the question'),
  type: z.string().describe('The type of the question'),
  // type: z.enum(['text', 'textarea', 'number']).describe('The type of the question'),
});
export const textItemSchema = z.object({
  type: z.literal('text'),
  locale: z.object({
    content: z.string().describe('text content of the presentation item'),
  }),
  description: z.string().optional().describe('The description of the option'),
}).describe('a text item, containing text content in markdown format');

export const sectionSchema = z.object({
  type: z.literal('section'),
  locale: z.object({
    heading: z.string().optional().describe('The heading of the section'),
  }),
  description: z.string().optional().describe('The description of the option'),
  items: z.array(baseQuestionSchema).describe('The questions or presentation items in the section'), // Allow both questions and presentation items
  // items: z.array(z.union([questionSchema, textItemSchema])).describe('The questions or presentation items in the section'), // Allow both questions and presentation items
}).describe('The section containing questions or presentation items');

export const pageSchema = z.object({
  type: z.literal('page').describe('Type for the page'),
  locale: z.object({
    heading: z.string().describe('The heading/title of the page'),
  }),
  description: z.string().optional().describe('The description of the option'),
  items: z.array(sectionSchema).describe('The sections in the page'),
}).describe('The page containing sections');

export const formSchema = z.object({
  type: z.literal('form').describe('Type for the form'),
  locale: z.object({
    heading: z.string().describe('title of the form'),
  }),
  description: z.string().optional().describe('The description of the option'),
  items: z.array(pageSchema).describe('The pages in the form'),
}).describe('The form containing pages');

const systemPromptForForm = `
  You are a editor of a Form. A Form is a collection of pages, sections, questions and text. 
  Each page has heading. Each page has section items. Each page can have a description. 
  Each section can have a heading and a description. Section group questions together under a common theme. 
  Section also contain text items written in markdown. A text item is a presentation item, it only contains content.
  Question types can be one of the following: text, textarea, number. 

  
`
const ai = genkit({
  plugins: [googleAI({ apiKey: process.env.GEMINI_API_KEY! })],
  model: gemini25FlashPreview0417, // set default model
});


async function main() {
  // make a generation request
  const { text } = await ai.generate({
    model: gemini25FlashPreview0417,
    system: systemPromptForForm,
    prompt: `Create a form that will be used to collect data from users about satisfaction with their current workplace.`,
    output: { schema: formSchema },
  });
  console.log(text);
}

main();

produces:

Restarting 'src/ai/generateForm.ts'
Shutting down all Genkit servers...
node:internal/process/promises:394
    triggerUncaughtException(err, true /* fromPromise */);
    ^

GoogleGenerativeAIFetchError: [GoogleGenerativeAI Error]: Error fetching from https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-04-17:generateContent: [400 Bad Request] Invalid JSON payload received. Unknown name "const" at 'generation_config.response_schema.properties[0].value': Cannot find field.
Invalid JSON payload received. Unknown name "const" at 'generation_config.response_schema.properties[3].value.items.properties[0].value': Cannot find field.
Invalid JSON payload received. Unknown name "const" at 'generation_config.response_schema.properties[3].value.items.properties[3].value.items.properties[0].value': Cannot find field. [{"@type":"type.googleapis.com/google.rpc.BadRequest","fieldViolations":[{"field":"generation_config.response_schema.properties[0].value","description":"Invalid JSON payload received. Unknown name \"const\" at 'generation_config.response_schema.properties[0].value': Cannot find field."},{"field":"generation_config.response_schema.properties[3].value.items.properties[0].value","description":"Invalid JSON payload received. Unknown name \"const\" at 'generation_config.response_schema.properties[3].value.items.properties[0].value': Cannot find field."},{"field":"generation_config.response_schema.properties[3].value.items.properties[3].value.items.properties[0].value","description":"Invalid JSON payload received. Unknown name \"const\" at 'generation_config.response_schema.properties[3].value.items.properties[3].value.items.properties[0].value': Cannot find field."}]}]
    at handleResponseNotOk (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected]/node_modules/@google/generative-ai/dist/index.js:434:11)
    at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
    at async makeRequest (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected]/node_modules/@google/generative-ai/dist/index.js:403:9)
    at async generateContent (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected]/node_modules/@google/generative-ai/dist/index.js:867:22)
    at async ChatSession.sendMessage (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected]/node_modules/@google/generative-ai/dist/index.js:1210:9)
    at async callGemini (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected][email protected]/node_modules/@genkit-ai/googleai/src/gemini.ts:1045:26)
    at async <anonymous> (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected][email protected]/node_modules/@genkit-ai/googleai/src/gemini.ts:1102:11)
    at async <anonymous> (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected]/node_modules/@genkit-ai/core/src/action.ts:439:14)
    at async <anonymous> (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected]/node_modules/@genkit-ai/core/src/action.ts:335:26)
    at async <anonymous> (/home/christophe/Programming/preignition/app/app-survey/functions/node_modules/.pnpm/@[email protected]/node_modules/@genkit-ai/core/src/tracing/instrumentation.ts:73:16) {
  status: 400,
  statusText: 'Bad Request',
  errorDetails: [
    {
      '@type': 'type.googleapis.com/google.rpc.BadRequest',
      fieldViolations: [
        {
          field: 'generation_config.response_schema.properties[0].value',
          description: `Invalid JSON payload received. Unknown name "const" at 'generation_config.response_schema.properties[0].value': Cannot find field.`
        },
        {
          field: 'generation_config.response_schema.properties[3].value.items.properties[0].value',
          description: `Invalid JSON payload received. Unknown name "const" at 'generation_config.response_schema.properties[3].value.items.properties[0].value': Cannot find field.`
        },
        {
          field: 'generation_config.response_schema.properties[3].value.items.properties[3].value.items.properties[0].value',
          description: `Invalid JSON payload received. Unknown name "const" at 'generation_config.response_schema.properties[3].value.items.properties[3].value.items.properties[0].value': Cannot find field.`
        }
      ]
    }
  ],
  traceId: '36cf7d26589b35ed1965c2ee010dd081'
}

Node.js v23.6.1
Failed running 'src/ai/generateForm.ts'

Expected behavior

  • zod basic features are not breaking schema parting
  • errors message convey useful information

Runtime (please complete the following information):

  • OS: Linux mint latest
  • Version [e.g. 22]

** Node version

  • 23.6.1

Additional context
For others hitting this, using enums as Literals seem to work better:

const typeEnums = z.enum([
  'text',
  'textarea',
  'number',
  'radio',
  'checkbox',
  'dropdown',
  'rating',
  'textfield',
  'tel',
  'date',
  'email',
  'url',
  'option',
  'section',
  'page',
  'form',
])

...
export const pageSchema = z.object({
  type: typeEnums.extract(['page']).describe('Type for the section'),
  locale: z.object({
    heading: z.string().describe('The heading/title of the page'),
  }),
  description: z.string().optional().describe('The description of the option'),
  items: z.array(sectionSchema).describe('The sections in the page'),
}).describe('The page containing sections');

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingjs

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions