LLM이 유효한 JSON을 반환하도록 기도하지 마십시오: Next.js에서 게이트 레벨 스키마를 적용하는 방법 15

작성자

카테고리:

← 피드로
DEV Community · Shahdin Salman · 2026-07-25 개발(SW)

Why JSON.parse() in your API handler is a production incident waiting to happen and how we engineer deterministic gateway validation at SpaceAI360.

Every developer building AI-powered features goes through the exact same honeymoon phase.

You write a neat system prompt asking Gemini or Claude to return a structured JSON response:

Return ONLY a valid JSON object matching this structure:
{ "summary": string, "sentiment": "positive" | "negative", "score": number }

Enter fullscreen mode Exit fullscreen mode

Do not include markdown code blocks or conversational text.
In 90% of your tests, it works flawlessly. You push to production, grab a coffee, and feel like a genius.

Then at 2 AM, a client enters an edge case payload (or the provider updates their model weights). The LLM suddenly wraps the response in triple backticks:

{
  "summary": "User reported a bug.",
  "sentiment": "negative",
  "score": "8/10" // <-- Oops, string instead of number
}

Enter fullscreen mode Exit fullscreen mode

Your backend executes JSON.parse(), crashes on the unescaped markdown or type mismatch, returns an unhandled 500 Internal Server Error to your frontend, and breaks the UI state.

At SpaceAI360, we learned this rule early: Never let unvalidated LLM output cross your API boundary.

Here is the exact production-ready pattern we use in Next.js 15 (Route Handlers) to guarantee 100% deterministic JSON schemas before data ever touches our database or client applications.

The Flawed Approach: Trusting JSON.parse()

Most teams handle LLM responses like this:

// ❌ BAD: Fragile parsing that throws unhandled exceptions
const rawText = await callLLM(prompt);
const data = JSON.parse(rawText); // Crushes if model adds backticks or bad types
await db.insert(data);
If JSON.parse() fails, your server thread blows up. If the type is slightly off (e.g., "8/10" instead of 8), your DB schema migration throws a constraint violation.

Enter fullscreen mode Exit fullscreen mode

The Solution: Gateway Schema Enforcement with Zod & Fallback Parsing
We move validation out of the application logic and enforce it strictly at the API Gateway level.

Here is our production setup in Next.js 15 using TypeScript, Zod, and structured stripping.

Step 1: Define the Strict Target Schema

TypeScript
// lib/schemas/analysis.ts
import { z } from 'zod';

export const AnalysisResponseSchema = z.object({
  summary: z.string().min(5),
  sentiment: z.enum(['positive', 'neutral', 'negative']),
  score: z.number().min(0).max(100),
  keyTopics: z.array(z.string()).default([]),
});

export type AnalysisResponse = z.infer<typeof AnalysisResponseSchema>;

Enter fullscreen mode Exit fullscreen mode

Step 2: The Sanitization & Validation Helper
Before passing raw text to Zod, strip common model markdown artifacts safely.

TypeScript
// lib/utils/sanitize-json.ts
export function extractCleanJson(rawString: string): string {
  // Strip markdown code fences if present (e.g., ```
{% endraw %}
json ...
{% raw %}
 ```)
  const cleaned = rawString
    .replace(/^```
{% endraw %}
(?:json)?\s*/i, '')
    .replace(/\s*
{% raw %}
```$/, '')
    .trim();

  return cleaned;
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Production Route Handler with Automatic Retries
If an LLM returns invalid JSON, do not crash the HTTP request. Use a single-retry repair loop before failing gracefully.

TypeScript
// app/api/analyze/route.ts
import { NextResponse } from 'next/server';
import { AnalysisResponseSchema } from '@/lib/schemas/analysis';
import { extractCleanJson } from '@/lib/utils/sanitize-json';

export async function POST(req: Request) {
  try {
    const { payload } = await req.json();

    let attempts = 0;
    const maxAttempts = 2;
    let validatedData = null;

    while (attempts < maxAttempts) {
      attempts++;

      // 1. Fetch raw output from LLM provider
      const rawLLMOutput = await callYourLLMProvider(payload, attempts > 1);

      // 2. Sanitize backticks / whitespace
      const cleanJsonString = extractCleanJson(rawLLMOutput);

      try {
        // 3. Attempt native JSON parse
        const parsedJson = JSON.parse(cleanJsonString);

        // 4. Enforce strict Zod Type Checking
        const parseResult = AnalysisResponseSchema.safeParse(parsedJson);

        if (parseResult.success) {
          validatedData = parseResult.data;
          break; // Schema is valid, break loop!
        } else {
          console.warn(`[Attempt ${attempts}] Zod Validation Failed:`, parseResult.error.format());
        }
      } catch (parseError) {
        console.warn(`[Attempt ${attempts}] JSON.parse failed on output.`);
      }
    }

    // If both attempts fail, return a structured fallback response instead of a 500 crash
    if (!validatedData) {
      return NextResponse.json(
        {
          error: 'SCHEMA_VALIDATION_FAILED',
          message: 'The model failed to produce a compliant payload after retry.',
        },
        { status: 422 }
      );
    }

    // 5. Safe to process downstream with 100% type safety
    return NextResponse.json({ success: true, data: validatedData }, { status: 200 });

  } catch (error) {
    return NextResponse.json({ error: 'INTERNAL_SERVER_ERROR' }, { status: 500 });
  }
}

Enter fullscreen mode Exit fullscreen mode

Architectural Benefits

Zero Runtime UI Crashes: Your frontend components can rely on exact TypeScript interfaces without worrying if a property might be undefined or wrongly typed.

Deterministic DB Writes: Database inserts will never fail due to invalid types or missing fields.

Graceful Degradation: If the model fails twice, your API returns a clean 422 Unprocessable Entity error rather than hanging or returning a generic 500.

The Takeaway for AI Engineering in 2026

LLMs are fundamentally non-deterministic engines. Expecting them to behave like rigid REST endpoints without external guardrails is a flaw in backend design.

Treat model responses like untrusted user inputs: Sanitize, Validate with Zod, and Enforce Hard Fallbacks.

How is your backend team handling schema validation when working with external AI providers? Let’s discuss in the comments!

Shahdin Salman

Founder, SpaceAI360

We engineer high-performance web applications, decoupled backend architectures, and production-grade automation systems.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다