Schema Design in Context Engineering

By the Syncfusion DevTutorials team · Last updated

How to give the model's reply a fixed shape with JSON Schema, Markdown or Pydantic, add fields for confidence and missing information, and check every reply in a validation pipeline.

Think about a form at the doctor's office. It has fixed boxes: name, date of birth, allergies, current medicines. Every patient fills in the same boxes. The nurse can read any form in seconds, and a computer can store it without mistakes.

Now imagine every patient wrote a free letter instead. Some would forget their allergies. Some would write their birthday in the middle of a story. The nurse would have to search every letter by hand.

A model's reply is like that letter, unless you give it a form. That form is called an output schema.

The main idea: tell the model the exact shape of the reply, then check the reply with code before you use it.

A Real-World Example

Think about a delivery app showing an order.

  • The app needs the order number, the status, and the delivery time, each in its own place.
  • If the server sent a paragraph like "Your order is on its way and should arrive soon", the app could not show a progress bar or a time.
  • So the server always sends the same fields, in the same shape, every time.

When a model's reply feeds into an app, it must behave like that server.

In simple words: a schema is a fill-in form for the model's reply.

What You Will Learn

In this lesson, you will learn:

  1. What an output schema is, and three ways to write one: JSON Schema, a Markdown schema, and a Pydantic model.
  2. The standard fields that make answers checkable.
  3. How to show uncertainty with confidence scores and missing information.
  4. How to check replies with code in a validation pipeline.
  5. What to do when a reply fails a check.

The Builder has a new JSON schema part, and a Check the reply box. Use the check json: answer, source, confidence, missing to see whether the model's reply is valid JSON with all four fields.

Output Schemas

An output schema describes the reply's shape: which fields it has, what type each field is, and which fields are required.

You met output schemas in Lesson 1, as one of the 14 context parts. In this lesson you will design them properly.

JSON Schema

JSON is a text format for structured data. It uses { } for objects, [ ] for lists, and "key": value pairs. Almost every programming language can read it.

JSON Schema is a standard way to describe what valid JSON looks like. Here is a schema for a support answer:

{
  "type": "object",
  "required": ["answer", "source", "confidence", "missing"],
  "properties": {
    "answer": { "type": "string" },
    "source": { "type": ["string", "null"] },
    "confidence": { "type": "number", "minimum": 0, "maximum": 1 },
    "missing": { "type": "array", "items": { "type": "string" } }
  }
}

It says: the reply is an object with four required fields. The answer is text. The source is text, or null when there is none. The confidence is a number from 0 to 1. The missing field is a list of text.

Many AI services can take a JSON Schema directly and force the reply to match it. This is often called structured outputs or JSON mode. Use it when your service has it. It is more reliable than asking in words alone.

When code receives a reply in this shape, reading it is easy:

A JSON reply is easy for code to use

const reply = '{"answer": "Your export failed because the Free plan allows 5,000 rows.", "source": "doc-1", "confidence": 0.9, "missing": []}';

const data = JSON.parse(reply);

console.log("Answer:", data.answer);
console.log("Source:", data.source);
console.log("Confidence:", data.confidence);
console.log("Missing:", data.missing.length === 0 ? "nothing" : data.missing.join(", "));

Expected output:

Answer: Your export failed because the Free plan allows 5,000 rows.
Source: doc-1
Confidence: 0.9
Missing: nothing

Try it with the real model. The card switches on the JSON schema part and checks the reply.

If the check fails, read the reason. Small models often add a sentence before the JSON, or forget a field. That is exactly why the next sections add validation.

Markdown Schema

Not every reply goes to code. Some go to a person. For those, a Markdown schema works well: a fixed set of headings the reply must use.

For example: "Reply with three sections: ## Answer, ## Steps, and ## Source."

A Markdown schema is easier to read, but weaker to check. Code can only test whether the headings are there.

Check that every required heading is there

const required = ["## Answer", "## Steps", "## Source"];

function checkMarkdown(reply) {
  const missing = required.filter((h) => !reply.includes(h));
  return missing.length ? "missing: " + missing.join(", ") : "all sections present";
}

const good = "## Answer\nThe Free plan allows 5,000 rows.\n\n## Steps\n1. Split the export.\n\n## Source\ndoc-1";
const bad = "The Free plan allows 5,000 rows. Split the export.";

console.log(checkMarkdown(good));
console.log(checkMarkdown(bad));

Expected output:

all sections present
missing: ## Answer, ## Steps, ## Source
JSON schemaMarkdown schema
Best forReplies read by codeReplies read by people
Easy to read for peopleLessYes
Easy to check with codeYes, field by fieldOnly the headings

Pydantic Models

In Python, many teams describe the schema as a Pydantic model. Pydantic is a Python library that checks data against a class. The same class can also produce a JSON Schema to send to the model.

The code below is shown, not run. This course runs JavaScript in your browser.

from typing import Optional
from pydantic import BaseModel, Field

class SupportAnswer(BaseModel):
    answer: str
    source: Optional[str]
    confidence: float = Field(ge=0, le=1)
    missing: list[str]

# Turn the model's reply into a checked object.
# A wrong type or a missing field raises a ValidationError.
reply = SupportAnswer.model_validate_json(raw_reply_text)

The class says the same thing as the JSON Schema above, but in Python. Field(ge=0, le=1) means "greater than or equal to 0, and less than or equal to 1". If the reply breaks any rule, Pydantic raises an error, and your code can react.

In TypeScript, the library Zod plays the same role.

Standard Fields

Some fields are useful in almost every schema, because they make an answer checkable. A good starting set is:

FieldWhat it holdsWhy it helps
ClaimThe statement the answer makesOne clear thing to check
SourceThe document id it came fromCode can look it up
EvidenceThe supporting quoteCode can find it in the source
ConfidenceHow sure the system is, from 0 to 1Decides whether to show, warn, or escalate
Follow-upWhat still needs checking or askingTurns gaps into next steps

You don't need every field every time. But a schema with a source and evidence field is one you can check. A schema with only an "answer" field is not.

Easy Way to Remember: a good reply schema answers what, where from, how sure, and what's missing.

Uncertainty Handling

A model always sounds sure. A schema can make it say how sure it is, and what it does not know.

Confidence Scores

A confidence score is a number, usually from 0 to 1, that says how sure the answer is. 0.9 means very sure. 0.3 means a guess.

Be careful: a model's own confidence number is not always reliable. Small models often say 0.9 for everything. Treat it as one signal, and combine it with the evidence checks from Lesson 6.

Missing Information

A missing field lists the facts the model needed but could not find. It turns "I'm not sure" into something useful: a list of what to ask or fetch next.

Asking for this field also helps the model stay honest. Instead of filling a gap with a guess, it has a proper place to put the gap.

The next box decides what to do with a reply, based on these two fields.

Let confidence and missing decide the next step

function nextAction(reply) {
  if (reply.missing.length > 0) return "ask the user for: " + reply.missing.join(", ");
  if (reply.confidence < 0.6) return "send to a human to review";
  return "show the answer";
}

console.log(nextAction({ confidence: 0.92, missing: [] }));
console.log(nextAction({ confidence: 0.45, missing: [] }));
console.log(nextAction({ confidence: 0.8, missing: ["number of rows"] }));

Expected output:

show the answer
send to a human to review
ask the user for: number of rows

The same model, three replies, three different next steps. None of them needed a person to read the reply first.

Schema Validation

Asking for a shape is not enough. Models sometimes break it: extra text around the JSON, a missing field, a word where a number should be. Validation means checking every reply with code before you use it.

Validation Pipelines

Every reply is cleaned, parsed, and checked before it is used. A failure at any step leads to a follow-up action.

A validation pipeline is a series of checks every reply passes through, in order:

  1. Clean. Remove extra wrapping, like a Markdown code fence around the JSON.
  2. Parse. Turn the text into data. If it is not valid JSON, it fails here.
  3. Check the shape. Every required field is there, with the right type.
  4. Check the rules. Confidence is between 0 and 1. The source exists. The quote is in the source.

Automated Validation

Automated validation means code does these checks, every time, with no person needed. The next box checks fields and types against a small schema.

Check every field and its type

const schema = {
  answer: "string",
  source: "string-or-null",
  confidence: "number",
  missing: "array",
};

function validate(obj) {
  const errors = [];
  for (const [field, type] of Object.entries(schema)) {
    const v = obj[field];
    if (!(field in obj)) errors.push(field + " is missing");
    else if (type === "array" && !Array.isArray(v)) errors.push(field + " must be a list");
    else if (type === "string-or-null" && v !== null && typeof v !== "string") errors.push(field + " must be text or null");
    else if ((type === "string" || type === "number") && typeof v !== type) errors.push(field + " must be a " + type);
  }
  if (typeof obj.confidence === "number" && (obj.confidence < 0 || obj.confidence > 1)) errors.push("confidence must be from 0 to 1");
  return errors.length ? errors : ["valid"];
}

console.log(validate({ answer: "5,000 rows", source: "doc-1", confidence: 0.9, missing: [] }).join("; "));
console.log(validate({ answer: "5,000 rows", confidence: "high", missing: "none" }).join("; "));

Expected output:

valid
source is missing; confidence must be a number; missing must be a list

The second reply has three problems. The validator names each one. In real projects, use a library such as Ajv for JSON Schema, Zod in TypeScript, or Pydantic in Python, instead of writing your own.

Follow-Up Actions

When a reply fails, the pipeline needs a follow-up action. Common ones are:

  • Retry: ask the model again, and include the error, such as "Your reply was missing the source field."
  • Repair: fix small problems in code, like removing a code fence.
  • Fall back: show a safe message, such as "I couldn't answer that. A person will follow up."
  • Escalate: send the case to a human.

Limit retries, for example to two. A model that failed twice in the same way will often fail a third time.

A small pipeline: clean, parse, check, decide

function stripFence(text) {
  return text.trim().replace(/^```(json)?\s*/, "").replace(/```$/, "").trim();
}

function pipeline(raw) {
  let data;
  try {
    data = JSON.parse(stripFence(raw));
  } catch {
    return "retry: reply was not valid JSON";
  }
  const needed = ["answer", "source", "confidence", "missing"];
  const missing = needed.filter((k) => !(k in data));
  if (missing.length) return "retry: missing " + missing.join(", ");
  return "accept: " + data.answer;
}

const replies = [
  '{"answer": "5,000 rows", "source": "doc-1", "confidence": 0.9, "missing": []}',
  '```json\n{"answer": "5,000 rows", "source": "doc-1", "confidence": 0.9, "missing": []}\n```',
  'Sure! The answer is 5,000 rows.',
  '{"answer": "5,000 rows"}',
];

for (const r of replies) console.log(pipeline(r));

Expected output:

accept: 5,000 rows
accept: 5,000 rows
retry: reply was not valid JSON
retry: missing source, confidence, missing

The second reply was wrapped in a code fence. The clean step repaired it, so it was accepted. The other two failed for different reasons, and each one gets a clear retry message.

Never pass a model's reply straight into your app, a database, or another tool. Parse it and validate it first, every time.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Vague format request"Reply in JSON."List every field, its type, and which are required.Each reply has different fields.
No validation"I asked for JSON, so it will be JSON."Parse and check every reply in code.One bad reply crashes the app.
Answer field only"The answer is all I need."Add source, evidence, confidence, and missing.Answers can't be checked or acted on.
Trusting confidence alone"It said 0.95, so it's right."Combine it with evidence checks.Confident wrong answers get shown.
Endless retries"Keep asking until it works."Retry once or twice, then fall back or escalate.Slow replies and wasted cost.
Markdown for machines"Headings are a schema too."Use JSON when code must read the fields.Code has to guess where the values are.

Exercises

Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.

What to Remember

  • An output schema is a fill-in form for the reply: fields, types, and required parts.
  • Use JSON Schema (or Pydantic in Python, Zod in TypeScript) when code reads the reply. Use a Markdown schema when people read it.
  • Good standard fields: claim, source, evidence, confidence, and follow-up.
  • Show uncertainty with a confidence score and a missing list, and let them decide the next step.
  • Run every reply through a validation pipeline: clean, parse, check the shape, check the rules.
  • On failure, retry with the error, repair, fall back, or escalate, and limit retries.

Next: Memory Management, the start of Chapter 3, on how memory is stored, updated, and kept private over time.