Skip to tool
FeuTex · free tools runs in-browser no bloat built by LiMiT

Schema Formatter Online

Format, minify, and validate Schema.org JSON-LD (structured data) in your browser—no uploads. Paste raw JSON-LD (or a full <script type="application/ld+json"> block) and get clean, readable output.

Category: SEO · URL: /tools/schema-formatter-online.html
Tip: You can paste a full JSON-LD <script type="application/ld+json"> block—this tool will extract the JSON.
Status: —
@type: —
@context: —
Privacy: runs locally in your browser. No uploads, no tracking scripts.

How to use

Use this tool to clean up Schema.org JSON-LD before adding it to your page.

  1. Paste your JSON-LD into the input (raw JSON or a full JSON-LD <script> block).
  2. Choose options (indent size, sort keys, try repair).
  3. Click Format or Minify.
  4. Copy or download the output, then place it inside <script type="application/ld+json">...</script>.
Keywords this page targets (natural cluster): schema formatter online, json-ld formatter, schema.org json-ld formatter, structured data formatter, json-ld minify, schema json formatter, format json-ld script tag, validate json-ld, pretty print json-ld, minify schema markup, json-ld prettifier, schema markup formatter, fix json-ld trailing comma, sort json keys online, schema json-ld validator offline, format product schema json-ld, format organization schema, format faq schema json-ld, format breadcrumb schema, schema script tag cleaner
Secondary intents covered: Make JSON-LD readable for debugging and reviews, Minify JSON-LD before publishing to reduce page weight, Extract JSON from a JSON-LD <script> tag and clean it, Validate JSON syntax and catch parse errors fast, Repair common JSON-LD paste issues (smart quotes, trailing commas), Sort keys for consistent diffs in version control, Copy or download cleaned JSON-LD for CMS/HTML insertion, Check basic schema signals like @context and @type presence

FAQ

What input formats does this accept?

It accepts strict JSON (object or array) and also extracts JSON from a JSON-LD <script type="application/ld+json"> block.

Does this validate Schema.org rules?

It validates JSON syntax and shows basic signals like @context and @type; it does not perform full Schema.org or Google rich result validation.

What does “Try repair” do?

It attempts safe fixes like converting smart quotes and removing trailing commas, then parses again as strict JSON.

Should I sort keys in JSON-LD?

Sorting is optional; it can make diffs cleaner, but it does not change meaning for JSON-LD.

Can I output a ready-to-paste script tag?

Yes—enable “Wrap in <script>” to generate a complete JSON-LD script block.

Will this tool send my schema to a server?

No. Everything runs locally in your browser and the tool JavaScript does not make network requests.

Why is my JSON-LD still invalid after repair?

Some issues (like missing quotes around keys, comments, or single-quoted strings) are not valid JSON; fix those and format again.

","expected":"Output contains \"@type\": \"Product\" and formatted newlines"},{"name":"Repair trailing comma parses to {\"a\":1}","input":"{\"a\":1,}","expected":"{\"a\":1}"},{"name":"Sort keys + schema priority puts @context first","input":"{\"name\":\"X\",\"@type\":\"Thing\",\"@context\":\"https://schema.org\"}","expected":"Formatted output starts with {\\n \"@context\""},{"name":"Array JSON-LD minifies correctly","input":"[{\"@type\":\"Thing\",\"name\":\"A\"},{\"@type\":\"Thing\",\"name\":\"B\"}]","expected":"[{\"@type\":\"Thing\",\"name\":\"A\"},{\"@type\":\"Thing\",\"name\":\"B\"}]"}]; // The tool JS must attach: // - window.feutexReset(): resets UI // - window.feutexSelfTest(tests): returns string log (PASS/FAIL) // - window.feutexInit(): optional (function(){ const log = document.getElementById('selfTestLog'); const btn = document.getElementById('selfTestBtn'); const resetBtn = document.getElementById('resetBtn'); function setLog(txt){ if(log) log.textContent = txt || ''; } btn?.addEventListener('click', async () => { try { if(typeof window.feutexSelfTest !== 'function') { setLog('Self-test missing: window.feutexSelfTest not defined.'); return; } const out = await window.feutexSelfTest(FEUTEX_SELF_TESTS); setLog(out || 'No self-test output.'); } catch(e) { setLog('Self-test error: ' + (e?.message || e)); } }); resetBtn?.addEventListener('click', () => { try { if(typeof window.feutexReset === 'function') window.feutexReset(); setLog(''); } catch(e) { setLog('Reset error: ' + (e?.message || e)); } }); try { if(typeof window.feutexInit === 'function') window.feutexInit(); } catch(_e) {} document.getElementById('yr').textContent = String(new Date().getFullYear()); })(); const re = /]*type\s*=\s*(["'])application\/ld\+json\1[^>]*>([\s\S]*?)<\/script>/i; const m = s.match(re); return m ? m[2].trim() : s.trim(); } function extractLikelyJson(str) { const s = String(str).trim(); // If it already starts with { or [, keep it. if (/^[\[{]/.test(s)) return s; // Otherwise try to locate the first { or [ and last matching } or ] (best-effort) const firstObj = s.indexOf('{'); const firstArr = s.indexOf('['); let start = -1; if (firstObj === -1) start = firstArr; else if (firstArr === -1) start = firstObj; else start = Math.min(firstObj, firstArr); if (start === -1) return s; const lastObj = s.lastIndexOf('}'); const lastArr = s.lastIndexOf(']'); let end = -1; if (lastObj === -1) end = lastArr; else if (lastArr === -1) end = lastObj; else end = Math.max(lastObj, lastArr); if (end === -1 || end <= start) return s; return s.slice(start, end + 1).trim(); } function stripTrailingCommas(jsonText) { // Repeatedly remove trailing commas before } or ] let s = String(jsonText); for (let i = 0; i < 10; i++) { const next = s.replace(/,\s*(\}|\])/g, '$1'); if (next === s) break; s = next; } return s; } function tryParseJson(input, { repair }) { const raw = stripHtmlWrapper(input); const extracted = extractLikelyJson(raw); try { return { ok: true, value: JSON.parse(extracted), used: extracted }; } catch (e1) { if (!repair) return { ok: false, error: e1, used: extracted }; // Repair pipeline (best-effort, still strict JSON in the end) let repaired = extracted; repaired = normalizeSmartQuotes(repaired); repaired = stripTrailingCommas(repaired); // Remove BOM repaired = repaired.replace(/^\uFEFF/, ''); try { return { ok: true, value: JSON.parse(repaired), used: repaired, repaired: true }; } catch (e2) { return { ok: false, error: e2, used: repaired }; } } } const SCHEMA_PRIORITY = [ '@context', '@type', '@id', 'name', 'headline', 'description', 'url', 'image', 'logo', 'sameAs', 'mainEntityOfPage', 'author', 'publisher', 'datePublished', 'dateModified', 'offers', 'aggregateRating', 'review', 'itemListElement' ]; function isPlainObject(v) { return v && typeof v === 'object' && !Array.isArray(v); } function sortObjectKeys(obj, schemaPriority) { const keys = Object.keys(obj); const pri = schemaPriority ? SCHEMA_PRIORITY : []; keys.sort((a, b) => { const ia = pri.indexOf(a); const ib = pri.indexOf(b); if (ia !== -1 || ib !== -1) { if (ia === -1) return 1; if (ib === -1) return -1; return ia - ib; } return a.localeCompare(b); }); const out = {}; for (const k of keys) out[k] = obj[k]; return out; } function deepTransform(value, { sortKeys, schemaPriority }) { if (Array.isArray(value)) return value.map(v => deepTransform(v, { sortKeys, schemaPriority })); if (isPlainObject(value)) { const base = sortKeys ? sortObjectKeys(value, schemaPriority) : value; const out = {}; for (const k of Object.keys(base)) { out[k] = deepTransform(base[k], { sortKeys, schemaPriority }); } return out; } return value; } function detectSchemaMeta(jsonValue) { let ctx = '—'; let types = '—'; const firstObj = Array.isArray(jsonValue) ? (jsonValue.find(isPlainObject) || null) : (isPlainObject(jsonValue) ? jsonValue : null); if (firstObj) { if (typeof firstObj['@context'] === 'string') ctx = firstObj['@context']; const t = firstObj['@type']; if (typeof t === 'string') types = t; else if (Array.isArray(t)) types = t.filter(x => typeof x === 'string').join(', ') || '—'; else types = '—'; } return { ctx, types }; } function wrapAsScript(jsonText) { const trimmed = String(jsonText).trim(); return ``; } function formatOutput(jsonValue, { indent, sortKeys, schemaPriority, wrapScript }) { const transformed = deepTransform(jsonValue, { sortKeys, schemaPriority }); const ind = indent === 0 ? 0 : indent; const text = JSON.stringify(transformed, null, ind); return wrapScript ? wrapAsScript(text) : text; } function setOutput(text) { els.output.value = text; } function getIndent() { const v = String(els.optIndent.value); const n = parseInt(v, 10); return Number.isFinite(n) ? n : 2; } function handleFormat(mode) { showError(''); const input = els.input.value; if (!input.trim()) { setMeta({ status: '—', types: '—', context: '—' }); setOutput(''); showError('Paste JSON-LD first.'); return; } const repair = !!els.optRepair.checked; const sortKeys = !!els.optSort.checked; const schemaPriority = !!els.optSchemaPriority.checked; const wrapScript = !!els.optWrapScript.checked; const indent = mode === 'minify' ? 0 : getIndent(); const parsed = tryParseJson(input, { repair }); if (!parsed.ok) { setMeta({ status: 'Invalid JSON', types: '—', context: '—' }); setOutput(''); const msg = parsed.error && parsed.error.message ? parsed.error.message : 'Invalid JSON.'; showError(`Parse error: ${msg}`); return; } const meta = detectSchemaMeta(parsed.value); const status = parsed.repaired ? 'Valid JSON (repaired)' : 'Valid JSON'; setMeta({ status, types: meta.types, context: meta.ctx }); try { const out = formatOutput(parsed.value, { indent, sortKeys, schemaPriority, wrapScript }); setOutput(out); } catch (e) { setOutput(''); setMeta({ status: 'Error', types: '—', context: '—' }); showError(`Formatting error: ${e && e.message ? e.message : String(e)}`); } } function validateOnly() { showError(''); const input = els.input.value; if (!input.trim()) { setMeta({ status: '—', types: '—', context: '—' }); showError('Paste JSON-LD first.'); return; } const repair = !!els.optRepair.checked; const parsed = tryParseJson(input, { repair }); if (!parsed.ok) { setMeta({ status: 'Invalid JSON', types: '—', context: '—' }); const msg = parsed.error && parsed.error.message ? parsed.error.message : 'Invalid JSON.'; showError(`Parse error: ${msg}`); return; } const meta = detectSchemaMeta(parsed.value); let status = parsed.repaired ? 'Valid JSON (repaired)' : 'Valid JSON'; if (meta.ctx !== '—' && typeof meta.ctx === 'string' && !/schema\.org/i.test(meta.ctx)) { status += ' (non-schema @context)'; } setMeta({ status, types: meta.types, context: meta.ctx }); } async function copyOutput() { showError(''); const text = els.output.value; if (!text.trim()) { showError('Nothing to copy.'); return; } try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); setMeta({ status: 'Copied to clipboard' }); } else { // Fallback els.output.focus(); els.output.select(); const ok = document.execCommand('copy'); setMeta({ status: ok ? 'Copied to clipboard' : 'Copy not supported' }); } } catch (e) { showError('Copy failed. Your browser may block clipboard access.'); } } function downloadOutput() { showError(''); const text = els.output.value; if (!text.trim()) { showError('Nothing to download.'); return; } const blob = new Blob([text], { type: 'application/ld+json;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'schema.jsonld'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } function loadSample() { const sample = ``; els.input.value = sample; handleFormat('format'); } function resetUI() { showError(''); els.input.value = ''; els.output.value = ''; els.optRepair.checked = true; els.optSort.checked = false; els.optSchemaPriority.checked = true; els.optWrapScript.checked = false; els.optIndent.value = '2'; setMeta({ status: '—', types: '—', context: '—' }); } function bind() { els.input = $('toolInput'); els.output = $('toolOutput'); els.error = $('toolError'); els.metaStatus = $('metaStatus'); els.metaTypes = $('metaTypes'); els.metaContext = $('metaContext'); els.optRepair = $('optRepair'); els.optSort = $('optSort'); els.optSchemaPriority = $('optSchemaPriority'); els.optWrapScript = $('optWrapScript'); els.optIndent = $('optIndent'); els.btnFormat = $('btnFormat'); els.btnMinify = $('btnMinify'); els.btnValidate = $('btnValidate'); els.btnCopy = $('btnCopy'); els.btnDownload = $('btnDownload'); els.btnSample = $('btnSample'); els.btnReset = $('btnReset'); els.btnFormat.addEventListener('click', () => handleFormat('format')); els.btnMinify.addEventListener('click', () => handleFormat('minify')); els.btnValidate.addEventListener('click', validateOnly); els.btnCopy.addEventListener('click', () => { void copyOutput(); }); els.btnDownload.addEventListener('click', downloadOutput); els.btnSample.addEventListener('click', loadSample); els.btnReset.addEventListener('click', () => window.feutexReset()); // UX: validate meta on input pause let t = null; els.input.addEventListener('input', () => { if (t) clearTimeout(t); t = setTimeout(() => { if (!els.input.value.trim()) { setMeta({ status: '—', types: '—', context: '—' }); showError(''); return; } // lightweight validate const parsed = tryParseJson(els.input.value, { repair: !!els.optRepair.checked }); if (parsed.ok) { const meta = detectSchemaMeta(parsed.value); setMeta({ status: parsed.repaired ? 'Valid JSON (repaired)' : 'Valid JSON', types: meta.types, context: meta.ctx }); showError(''); } else { setMeta({ status: 'Invalid JSON', types: '—', context: '—' }); } }, 250); }); resetUI(); } // Public API required by FeuTex window.feutexReset = function feutexReset() { try { resetUI(); } catch (_) {} }; window.feutexInit = function feutexInit() { bind(); }; window.feutexSelfTest = function feutexSelfTest() { const logs = []; const tests = [ { name: 'Extract JSON from '; const parsed = tryParseJson(input, { repair: true }); if (!parsed.ok) return { ok: false, detail: 'parse failed' }; const out = formatOutput(parsed.value, { indent: 2, sortKeys: false, schemaPriority: true, wrapScript: false }); return { ok: out.includes('"@type": "Product"') && out.includes('\n '), detail: out.slice(0, 60) }; } }, { name: 'Repair trailing comma parses to {"a":1}', run: () => { const input = '{"a":1,}'; const parsed = tryParseJson(input, { repair: true }); if (!parsed.ok) return { ok: false, detail: 'parse failed' }; const min = formatOutput(parsed.value, { indent: 0, sortKeys: false, schemaPriority: true, wrapScript: false }); return { ok: min === '{"a":1}', detail: min }; } }, { name: 'Sort keys + schema priority puts @context first', run: () => { const input = '{"name":"X","@type":"Thing","@context":"https://schema.org"}'; const parsed = tryParseJson(input, { repair: false }); if (!parsed.ok) return { ok: false, detail: 'parse failed' }; const out = formatOutput(parsed.value, { indent: 2, sortKeys: true, schemaPriority: true, wrapScript: false }).trim(); const ok = out.startsWith('{\n "@context"'); return { ok, detail: out.split('\n')[0] + ' / ' + out.split('\n')[1] }; } }, { name: 'Array JSON-LD minifies correctly', run: () => { const input = '[{"@type":"Thing","name":"A"},{"@type":"Thing","name":"B"}]'; const parsed = tryParseJson(input, { repair: false }); if (!parsed.ok) return { ok: false, detail: 'parse failed' }; const out = formatOutput(parsed.value, { indent: 0, sortKeys: false, schemaPriority: true, wrapScript: false }); return { ok: out === input, detail: out }; } } ]; let passCount = 0; for (const t of tests) { let res; try { res = t.run(); } catch (e) { res = { ok: false, detail: e && e.message ? e.message : String(e) }; } if (res.ok) passCount++; logs.push(`${res.ok ? 'PASS' : 'FAIL'}: ${t.name}${res.detail ? ' — ' + res.detail : ''}`); } logs.push(`Result: ${passCount}/${tests.length} passing`); return logs.join('\n'); }; // Auto-init if DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => window.feutexInit && window.feutexInit()); } else { window.feutexInit && window.feutexInit(); } })();