Concepts
The three ideas behind the API — literal types, verbatim options, and safe serialization.
Factories stamp a literal @type
Each schema interface declares its @type as a literal type, and each factory fills it in. You never write "@type" by hand, and you can’t misspell it:
const org = createOrganizationSchema({ name: "Acme" });
// org["@type"] is the literal "Organization"
createPersonSchema({ "@type": "Person", name: "x" });
// type error — options exclude "@type"; the factory owns it Because the literals survive, unions like StructuredDataSchema discriminate correctly, and passing the wrong node where a PersonSchema is expected fails at compile time.
Options pass through verbatim
Factories add nothing behind your back: no default language, no derived fields. The output object is { "@type": ..., ...yourOptions }, which has two consequences worth knowing:
- You own every value. If you want
inLanguage: "en", say so at the call site. - JSON key order follows your option order.
JSON.stringifyserializes keys in insertion order, so the emitted bytes are stable and snapshot-friendly. This is a tested guarantee, not an accident.
Identify nodes with @id, connect them with references
Give long-lived entities an @id — a URL you control, conventionally with a fragment:
const person = createPersonSchema({
"@id": "https://example.com/#person",
name: "Ada Lovelace",
}); Any other node can then point at it with a bare reference instead of repeating the data:
import { createNodeRef } from "structured-data-generators";
const article = createArticleSchema({
headline: "On the Analytical Engine",
datePublished: "1843-09-01",
isPartOf: createNodeRef("https://example.com/#website"),
}); Consumers that understand JSON-LD (Google does) resolve the reference to the full node — even one declared on a different page of your site. See Person entity graph for the full pattern.
Why the serializer escapes
A <script> element ends at the first </script> sequence — including one inside a JSON string. This is fine:
JSON.stringify({ name: "Ada" }); until a value contains user-supplied text:
JSON.stringify({ review: "nice site </script><script>steal(document.cookie)" }); That closes your JSON-LD block early and hands the browser a live script tag. toJsonLd escapes <, >, and & to \u003c, \u003e, \u0026 — valid JSON escapes that parse back to the same characters but can never terminate the element. It also escapes U+2028/U+2029, which are legal in JSON but historically broke JavaScript string contexts.
The escaping is transparent: JSON.parse(toJsonLd(x)) deep-equals { "@context": "https://schema.org/", ...x }.