Practice Problems Schema Framework
Use conditional-logic schema generation to keep Practice Problems markup aligned with real page state and avoid stale or invalid static templates.
- Google no longer shows Practice problems rich results: deprecation was announced Nov 5, 2025, and support was removed from Search Console rich result reporting, the Rich Results Test and Search appearance filters starting Jan 2026.
- Quiz and Question markup still carries schema.org meaning for AI assistants and other education platforms, and Education Q&A remains a separate, documented Google feature for flashcard pages.
- Move problems with one answer to Education Q&A flashcard markup. Keep multiple choice or checkbox markup only for non-Google consumers: Google's Education Q&A treats any non-Flashcard question as ineligible.
When To Use It
Google Search Central RetiredDocumentation changelogThe documentation page was removed in Jan 2026. Google still documents Quiz markup for Education Q&A flashcards, so the framework below converts open-answer problems to flashcards.
Key Implementation Documentation Highlights
- Google lists no required properties for Practice problems: the documentation is gone and the feature is retired.
- The live Google path for Quiz is Education Q&A: hasPart with Question items, each with eduQuestionType
Flashcard, text and exactly one acceptedAnswer with text. - Education Q&A recommends about (a Thing with name) and educationalAlignment (AlignmentObject with alignmentType
educationalSubjectoreducationalLeveland targetName). - Education Q&A is limited to education-related searches in English, Portuguese, Spanish (Mexico only) and Vietnamese.
- General structured data policies still apply: mark up only problems visible on the page and keep the page indexable.
Rules the framework enforces before it emits markup:
- Problems not visible on the page, or a noindex or canonicalized URL: emit nothing.
- Open problems with one answer become Flashcard questions; a problem without question or answer text suppresses the markup.
- Multiple choice and checkbox problems are left out by default, because one non-Flashcard question makes the whole Quiz ineligible for Education Q&A. Set includeChoiceQuestions only when a non-Google consumer needs them.
- Choice questions without a correct option are dropped, and answer positions are numbered from 1 in display order.
Template Approach
const pageData = {
title: "Linear equations practice set",
question: "Solve for x: 3x + 5 = 20",
answer: "x = 5"
};
const staticTemplate = {
"@context": "https://schema.org",
"@type": "Quiz",
"name": pageData.title,
"hasPart": [
{
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": pageData.question,
"acceptedAnswer": { "@type": "Answer", "text": pageData.answer }
}
]
};
{
"@context": "https://schema.org",
"@type": "Quiz",
"name": "Linear equations practice set",
"hasPart": [
{
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": "Solve for x: 3x + 5 = 20",
"acceptedAnswer": {
"@type": "Answer",
"text": "x = 5"
}
}
]
}
Conditional-logic Framework
function isAbsoluteUrl(value) {
if (typeof value !== "string") return false;
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:";
} catch {
return false;
}
}
function compact(value) {
if (Array.isArray(value)) {
const items = value.map(compact).filter((item) => item !== undefined);
return items.length ? items : undefined;
}
if (value && typeof value === "object") {
const out = {};
for (const [key, item] of Object.entries(value)) {
const kept = compact(item);
if (kept !== undefined) out[key] = kept;
}
return Object.keys(out).length ? out : undefined;
}
if (value === null || value === undefined || value === "") return undefined;
return value;
}
function chooseQuestionType(problem) {
// open problems with one answer map to Flashcard, the only Quiz question type Google still documents
const options = problem.options || [];
if (!options.length) return "Flashcard";
const correct = options.filter((o) => o.correct).length;
if (correct === 0) return null;
return correct === 1 ? "Multiple choice" : "Checkbox";
}
function buildQuestion(problem, type) {
const text = (problem.question || "").trim();
if (!text) return null;
if (type === "Flashcard") {
const answer = (problem.answer || "").trim();
if (!answer) return null;
return {
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": text,
"acceptedAnswer": { "@type": "Answer", "text": answer } // exactly one
};
}
// choice questions: schema.org semantics only, no Google feature reads them
const answers = problem.options.map((o, i) => ({ "@type": "Answer", "position": i + 1, "text": o.text }));
return {
"@type": "Question",
"eduQuestionType": type,
"text": text,
"suggestedAnswer": answers.filter((a, i) => !problem.options[i].correct),
"acceptedAnswer": answers.filter((a, i) => problem.options[i].correct)
};
}
function buildPracticeQuizSchema(source) {
// Practice problems rich results were retired in Jan 2026; Google now reads Quiz only as
// Education Q&A flashcards. Emit only for visible, indexable problem sets.
if (source.indexable === false) return null;
if (!isAbsoluteUrl(source.url)) return null;
if (source.canonicalUrl && source.canonicalUrl !== source.url) return null;
if (source.problemsVisible !== true) return null;
// choice questions make the Quiz ineligible for Education Q&A; include them only on request
const hasPart = (source.problems || [])
.map((p) => ({ problem: p, type: chooseQuestionType(p) }))
.filter(({ type }) => type === "Flashcard" || (type && source.includeChoiceQuestions === true))
.map(({ problem, type }) => buildQuestion(problem, type));
if (!hasPart.length || hasPart.includes(null)) return null;
const schema = {
"@context": "https://schema.org",
"@type": "Quiz",
"name": source.title,
"about": source.concept ? { "@type": "Thing", "name": source.concept } : null,
"educationalAlignment": (source.alignments || [])
.filter((a) => ["educationalSubject", "educationalLevel"].includes(a.type) && a.targetName)
.map((a) => ({ "@type": "AlignmentObject", "alignmentType": a.type, "targetName": a.targetName })),
"hasPart": hasPart
};
return compact(schema);
}
// Usage:
// buildPracticeQuizSchema({
// url: "https://www.example.com/algebra/linear-equations/practice",
// canonicalUrl: "https://www.example.com/algebra/linear-equations/practice",
// indexable: true,
// problemsVisible: true,
// title: "Linear equations practice set",
// concept: "Solving linear equations",
// alignments: [
// { type: "educationalSubject", targetName: "Algebra" },
// { type: "educationalLevel", targetName: "Grade 8" }
// ],
// problems: [
// { question: "Solve for x: 3x + 5 = 20", answer: "x = 5" },
// { question: "Solve for y: 2y - 7 = 9", answer: "y = 8" },
// { question: "Which value of x satisfies 4x = 28?", options: [{ text: "6" }, { text: "7", correct: true }, { text: "8" }] }
// ]
// });
from urllib.parse import urlparse
def is_absolute_url(value):
if not isinstance(value, str):
return False
parsed = urlparse(value)
return parsed.scheme in ("http", "https") and bool(parsed.netloc)
def compact(value):
if isinstance(value, list):
items = [kept for kept in (compact(item) for item in value) if kept is not None]
return items or None
if isinstance(value, dict):
out = {}
for key, item in value.items():
kept = compact(item)
if kept is not None:
out[key] = kept
return out or None
if value is None or value == "":
return None
return value
def choose_question_type(problem):
# open problems with one answer map to Flashcard, the only Quiz question type Google still documents
options = problem.get("options") or []
if not options:
return "Flashcard"
correct = sum(1 for o in options if o.get("correct"))
if correct == 0:
return None
return "Multiple choice" if correct == 1 else "Checkbox"
def build_question(problem, question_type):
text = (problem.get("question") or "").strip()
if not text:
return None
if question_type == "Flashcard":
answer = (problem.get("answer") or "").strip()
if not answer:
return None
return {
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": text,
"acceptedAnswer": {"@type": "Answer", "text": answer}, # exactly one
}
# choice questions: schema.org semantics only, no Google feature reads them
options = problem["options"]
answers = [{"@type": "Answer", "position": i + 1, "text": o.get("text")} for i, o in enumerate(options)]
return {
"@type": "Question",
"eduQuestionType": question_type,
"text": text,
"suggestedAnswer": [a for a, o in zip(answers, options) if not o.get("correct")],
"acceptedAnswer": [a for a, o in zip(answers, options) if o.get("correct")],
}
def build_practice_quiz_schema(source):
# Practice problems rich results were retired in Jan 2026; Google now reads Quiz only as
# Education Q&A flashcards. Emit only for visible, indexable problem sets.
if source.get("indexable") is False:
return None
if not is_absolute_url(source.get("url")):
return None
if source.get("canonicalUrl") and source["canonicalUrl"] != source["url"]:
return None
if source.get("problemsVisible") is not True:
return None
# choice questions make the Quiz ineligible for Education Q&A; include them only on request
typed = [(p, choose_question_type(p)) for p in source.get("problems") or []]
has_part = [
build_question(p, t)
for p, t in typed
if t == "Flashcard" or (t and source.get("includeChoiceQuestions") is True)
]
if not has_part or None in has_part:
return None
schema = {
"@context": "https://schema.org",
"@type": "Quiz",
"name": source.get("title"),
"about": {"@type": "Thing", "name": source["concept"]} if source.get("concept") else None,
"educationalAlignment": [
{"@type": "AlignmentObject", "alignmentType": a["type"], "targetName": a["targetName"]}
for a in source.get("alignments") or []
if a.get("type") in ("educationalSubject", "educationalLevel") and a.get("targetName")
],
"hasPart": has_part,
}
return compact(schema)
# Usage:
# build_practice_quiz_schema({
# "url": "https://www.example.com/algebra/linear-equations/practice",
# "canonicalUrl": "https://www.example.com/algebra/linear-equations/practice",
# "indexable": True,
# "problemsVisible": True,
# "title": "Linear equations practice set",
# "concept": "Solving linear equations",
# "alignments": [
# {"type": "educationalSubject", "targetName": "Algebra"},
# {"type": "educationalLevel", "targetName": "Grade 8"},
# ],
# "problems": [
# {"question": "Solve for x: 3x + 5 = 20", "answer": "x = 5"},
# {"question": "Solve for y: 2y - 7 = 9", "answer": "y = 8"},
# {"question": "Which value of x satisfies 4x = 28?", "options": [{"text": "6"}, {"text": "7", "correct": True}, {"text": "8"}]},
# ],
# })
{
"@context": "https://schema.org",
"@type": "Quiz",
"name": "Linear equations practice set",
"about": {
"@type": "Thing",
"name": "Solving linear equations"
},
"educationalAlignment": [
{
"@type": "AlignmentObject",
"alignmentType": "educationalSubject",
"targetName": "Algebra"
},
{
"@type": "AlignmentObject",
"alignmentType": "educationalLevel",
"targetName": "Grade 8"
}
],
"hasPart": [
{
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": "Solve for x: 3x + 5 = 20",
"acceptedAnswer": {
"@type": "Answer",
"text": "x = 5"
}
},
{
"@type": "Question",
"eduQuestionType": "Flashcard",
"text": "Solve for y: 2y - 7 = 9",
"acceptedAnswer": {
"@type": "Answer",
"text": "y = 8"
}
}
]
}
The Output As An Entity Graph
The conditional-logic output above, parsed in your browser and drawn as the graph a crawler builds from it. Every nested object is a node and the property that holds it is the edge. Tap a node to inspect its properties, or drag to rearrange.
Why Conditional Logic Is Better Than Static Templates
- Routes each problem to the right question type, so open problems keep a Google path through Education Q&A.
- Keeps choice questions out of the Google-facing markup by default instead of silently making the page ineligible.
- Builds answers from the same records that render the problem set, so markup cannot drift from visible content.
- Gives one place to change if Google changes Quiz support again.
Property Reference
Google retired the Practice Problems rich result, so its documentation no longer lists required or recommended properties. The framework above is still valid schema.org markup and still describes the page to search engines and AI assistants.
Schema.org types used