Last reviewed: 2026-08-09

Direct answer

Treat a model response as untrusted input even when you ask for JSON. The reliable pipeline has two contracts: CometAPI’s Gemini-native request contract and your application’s data contract. Ask Gemini for JSON with generationConfig.responseMimeType set to application/json, extract the text from the first candidate, parse it with JSON.parse, and then validate the resulting value with Ajv before passing it to business logic. The CometAPI Generate content documentation documents the native generateContent route, JSON mode, and the optional responseSchema field. Google’s GenerateContent API reference is the authority for the provider request and response shape.

This tutorial keeps the provider-side request deliberately small and makes Ajv the final gate. That separation is useful when a model does not support every schema keyword, when a model is changed, or when a gateway returns a safety or error response instead of a candidate. The example uses a current Node runtime with fetch and a runtime-injected credential. Set COMETAPI_BASE_URL to the host shown in the CometAPI documentation. Keep the credential outside the source tree and do not put it in logs.

Install Ajv once, save the sample as pipeline.mjs, and run it with a current Node runtime that provides fetch:

npm install ajv
node pipeline.mjs

The following program asks for a summary and scored items, checks the HTTP response, finds a text part, parses JSON, and validates the object. The schema is compiled once at startup, as recommended in the Ajv getting-started guide , rather than recompiled for every request.

import Ajv from "ajv";

const runtime = process.env;
if (!runtime.COMETAPI_BASE_URL || !runtime.COMETAPI_KEY) {
  throw new Error("Set COMETAPI_BASE_URL and COMETAPI_KEY in the environment.");
}

const model = "gemini-3-flash-preview";
const endpoint = `${runtime.COMETAPI_BASE_URL}/v1beta/models/${model}:generateContent`;
const requestHeaders = new Headers();
requestHeaders.set("content-type", "application/json");
requestHeaders.set("x-goog-api-key", runtime.COMETAPI_KEY);

const outputSchema = {
  type: "object",
  additionalProperties: false,
  required: ["summary", "items"],
  properties: {
    summary: { type: "string", minLength: 1 },
    items: {
      type: "array",
      minItems: 1,
      items: {
        type: "object",
        additionalProperties: false,
        required: ["label", "confidence"],
        properties: {
          label: { type: "string", minLength: 1 },
          confidence: { type: "number", minimum: 0, maximum: 1 }
        }
      }
    }
  }
};

const ajv = new Ajv({ allErrors: true, strict: true });
const validateOutput = ajv.compile(outputSchema);

const response = await fetch(endpoint, {
  method: "POST",
  headers: requestHeaders,
  body: JSON.stringify({
    contents: [
      {
        role: "user",
        parts: [
          {
            text: "Return a JSON object with a short summary and one or more scored items about: renewable energy. Do not use Markdown fences."
          }
        ]
      }
    ],
    generationConfig: {
      responseMimeType: "application/json",
      temperature: 0.2
    }
  })
});

const rawBody = await response.text();
let payload;
try {
  payload = JSON.parse(rawBody);
} catch {
  throw new Error(`CometAPI returned a non-JSON HTTP body (${response.status}).`);
}

const candidateCount = Array.isArray(payload.candidates)
  ? payload.candidates.length
  : 0;
const firstCandidate = payload.candidates?.[0];
console.info(JSON.stringify({
  event: "cometapi_gemini_json_response",
  model,
  http_status: response.status,
  candidate_count: candidateCount,
  finish_reason: firstCandidate?.finishReason ?? null,
  total_tokens: payload.usageMetadata?.totalTokenCount ?? null
}));

if (!response.ok) {
  throw new Error(`CometAPI request failed with HTTP ${response.status}.`);
}

const textPart = firstCandidate?.content?.parts?.find(
  (part) => typeof part.text === "string"
);
if (!textPart) {
  throw new Error("The response had no text candidate to parse.");
}

let value;
try {
  value = JSON.parse(textPart.text);
} catch {
  throw new Error("The candidate text was not valid JSON.");
}

if (!validateOutput(value)) {
  console.error(JSON.stringify({
    event: "cometapi_gemini_json_rejected",
    model,
    schema_valid: false,
    validation_errors: validateOutput.errors
  }));
  throw new Error("The candidate JSON did not match the application schema.");
}

console.info(JSON.stringify({
  event: "cometapi_gemini_json_accepted",
  model,
  schema_valid: true,
  item_count: value.items.length
}));
// Hand off `value` to application code; do not log its contents.

The program reads COMETAPI_KEY directly from the runtime environment, checks that it is present before sending, and never includes it in a log record. The .mjs filename makes the ESM import and top-level await explicit.

The happy path ends only after schema_valid is true. A successful HTTP status alone is not enough: it tells you that the gateway accepted the request, not that the model produced the object your application expects. Keep the validated value as the hand-off to persistence, rendering, or a downstream queue.

Who this is for

This pattern is for developers who:

  • call Gemini through CometAPI but want the native generateContent request shape;
  • need predictable objects for a database, queue, UI, or typed service boundary;
  • want a local validation gate that remains useful when model behavior or provider support changes;
  • maintain Node.js services where credentials and model output must stay out of source-controlled logs.

It is not a replacement for prompt design, moderation, or domain checks. Ajv can confirm that confidence is a number between zero and one; it cannot decide whether the model’s explanation is truthful.

Key takeaways

  • Use the Gemini-native CometAPI route for the JSON-mode contract described in the provider documentation. The native response uses candidates, content, and parts, not the choices and message fields used by an OpenAI-shaped chat response.
  • responseMimeType: "application/json" requests JSON output. CometAPI also documents an optional responseSchema; add it only after confirming that the selected model supports the schema subset you need.
  • Compile the Ajv validator once and reuse it. Compilation is setup work; validation belongs on every response.
  • Log request outcome metadata, not prompts, candidate text, or credentials. A small record with status, candidate count, finish reason, token count, and validation result is enough to operate the pipeline.
  • Reject malformed or schema-invalid objects. Do not silently strip fields or repair arbitrary text with regular expressions.

Sources checked

The CometAPI Generate content documentation specifies the native endpoint, the x-goog-api-key authentication option, Gemini response fields, JSON mode, and the optional responseSchema. It also warns that Gemini request parameters and response fields can change, so model-specific details should be rechecked before release.

The Google GenerateContent API reference defines the upstream GenerateContentRequest, GenerateContentResponse, contents, generationConfig, and response-format fields. Use it when you need to compare CometAPI’s gateway shape with Google’s current native contract.

The Ajv getting-started guide documents installing Ajv, compiling a schema into a validation function, reusing the compiled function, and reading validation errors. The JSON Schema first-schema guide explains how type, properties, required, and other validation keywords describe a JSON instance.

Together, these sources support a practical boundary: the gateway requests JSON, the JavaScript parser turns text into a value, and Ajv decides whether that value is admissible for your application.

Contract details to verify

Before shipping, verify five things against the current docs and the model catalog.

  1. Endpoint and operator. The native route is /v1beta/models/{model}:generateContent for a synchronous response. The streaming operator is a different operation and emits Server-Sent Events. This article buffers one complete response so that JSON parsing is atomic.
  2. Authentication placement. The example sends the environment-provided key in the x-goog-api-key header. Do not move a key into a URL query or commit it to a shell history file. Keep the header construction in one client module so the rest of the application never handles raw credentials.
  3. Model capability. gemini-3-flash-preview is the example model used by the refetched CometAPI page, but model IDs and supported parameters can change. Confirm the selected model in the CometAPI model catalog checklist before changing the example.
  4. Response extraction. A normal candidate contains content.parts, and a text part contains text. A safety block, tool call, empty candidate, or provider error may produce no usable text. Branch on the observed shape instead of assuming index zero always exists.
  5. Schema ownership. Keep outputSchema versioned with the consumer that uses it. If you also send a provider-side responseSchema, treat it as an optimization, not your only guard. The local Ajv schema is what protects the application boundary.

For background on reading nested response objects, see the CometAPI response-object guide . The link includes this article’s campaign tuple so the hand-off remains attributable.

A useful operator workflow is:

  1. At startup, load the base URL and key from the runtime environment and compile the schema. Fail fast if either setting is absent.
  2. For each request, send a short prompt, responseMimeType, and only the generation settings supported by the chosen model.
  3. Record the sanitized outcome fields shown in the example. Never include rawBody, textPart.text, the prompt, or the API key in that record.
  4. On the happy path, parse and validate, then pass the object to the next service.
  5. On the error path, classify the failure, retain the HTTP status and Ajv errors, and quarantine the input for inspection or a bounded retry. Do not present an unvalidated object as a successful result.

Failure modes

HTTP 401 or 403. The key is missing, expired, or not accepted for the account. Check the environment injection and header name. Because the sample never prints the key, the log can be shared without exposing a credential.

HTTP 400 for a request parameter. A model may reject an unsupported generation setting, an invalid model ID, or a schema keyword outside the provider’s supported subset. Start with responseMimeType only, confirm the model, and add optional schema constraints incrementally.

HTTP success with no candidate text. A response can contain an empty candidates array, a safety outcome, or a non-text part. Treat this as a failed application result. Log candidate_count and finish_reason, then apply your product’s safe fallback rather than parsing a missing value.

Candidate text is fenced or malformed. JSON.parse should fail closed. A prompt that says “no Markdown fences” reduces the chance of this error, but it does not remove the need for a parser check. If you retry, keep the attempt count bounded and preserve the original classification.

Valid JSON fails Ajv. The object may omit summary, add an unexpected property, use a string for confidence, or send an empty items array. Keep validateOutput.errors in a redacted diagnostic record, fix the prompt or schema deliberately, and do not coerce values silently.

Streaming is parsed as a complete object. The documented streaming operation emits separate SSE events. A partial chunk is not a complete JSON document. Buffer the final text before parsing, or use a streaming-aware incremental parser with its own tests; do not call JSON.parse on each chunk.

The provider contract drifts. CometAPI and Google both warn that native parameters and response fields can change. Schedule a small contract smoke test that checks the endpoint, candidate extraction, JSON mode, and schema validation against the model ID you actually deploy.

FAQ

Do I need the OpenAI-compatible chat endpoint?

No. CometAPI documents Gemini native JSON mode on the v1beta generateContent route. Use the native shape when you need Gemini-specific generation settings or response fields. The OpenAI-compatible endpoint has a different response shape and should not be mixed into this extractor.

Does JSON mode guarantee my exact object?

No application boundary should assume that. responseMimeType requests JSON output, and CometAPI documents an optional responseSchema for stricter provider-side output. Your service should still parse the text and validate the value with Ajv. That gives you a deterministic decision point even when a model refuses, truncates, or returns a field with the wrong type.

Why compile Ajv once?

Ajv turns a schema into a validation function. Compiling it once at startup avoids repeating setup work and keeps every request on the same schema version. If you change the schema, restart or rebuild the service so the validator and consumer change together.

Should I use a retry to fix invalid JSON?

Only under an explicit, bounded policy. A retry can help with transient transport or rate-limit conditions, but repeatedly asking a model to “fix” an invalid object can hide a contract problem. Record the failure class, retry only when the class is retryable, and send repeated schema failures to a review path.

Can I log the model text for debugging?

Avoid logging it by default. Candidate text can contain user data, personal information, or prompt material. Log status, model, candidate count, finish reason, token count, and Ajv error paths. If a controlled diagnostic capture is required, apply your data-retention and redaction policy outside this tutorial’s normal request path.

Reader next step

Create a small CometAPI key in your runtime secret store, choose a currently listed Gemini model, and run the sample against a harmless test prompt. First confirm the happy path produces schema_valid: true. Then exercise the error path with a deliberately missing field and verify that the record is rejected without printing the candidate text or key. When you are ready to try the integration, Start with CometAPI .