JSON-LD: The Complete Guide (Syntax, @context, Examples for SEO)

JSON-LD (JavaScript Object Notation for Linked Data) is a W3C Recommendation that expresses linked data as ordinary JSON, which means a JSON-LD document is both a normal object a JavaScript parser can read and a set of RDF triples a graph store can load. Version 1.0 became a W3C Recommendation in 2014 and version 1.1 in 2020. On the web it is best known as the format inside <script type="application/ld+json"> tags, the way most sites publish schema.org structured data.

What JSON-LD actually is

Plain JSON has keys but no shared meaning for them. Two APIs can both return "name" and mean different things, and nothing in the document says which. JSON-LD adds one idea: a context that maps each key to an IRI, a globally unique identifier for that property. Once "name" resolves to https://schema.org/name, it means the same thing everywhere.

That single addition turns JSON into a serialization of RDF. Every JSON-LD document can be converted to triples of subject, predicate, and object without loss, and back again, so a developer can treat it as JSON while a data engineer loads the same bytes into a triple store. See triples: subject, predicate, object for the underlying model and RDF for the family of syntaxes JSON-LD belongs to.

Here is a document with no context. It is valid JSON and meaningless to a machine:

{
  "name": "Marie Curie",
  "birthDate": "1867-11-07"
}

Adding a context and a type makes every key resolvable:

{
  "@context": "https://schema.org",
  "@type": "Person",
  "@id": "https://example.org/people/marie-curie",
  "name": "Marie Curie",
  "birthDate": "1867-11-07",
  "sameAs": "https://www.wikidata.org/wiki/Q7186"
}

The second document says that the thing identified by that URL is a https://schema.org/Person, that its https://schema.org/name is “Marie Curie”, and that it is the same entity as Wikidata item Q7186. Those are facts a graph can absorb.

The keywords that matter

JSON-LD reserves keys beginning with @. A handful carry most of the work.

@context maps terms to IRIs. It can be a URL ("https://schema.org"), an inline object, or an array combining both. Terms defined in a context can also carry a declared type or container, which is how typed and language-tagged values get shortened.

@type states the class of a node (Person, Product, Article) or the datatype of a value. In schema.org markup it is the property search engines key off first.

@id gives a node a stable identifier, normally a URL. Nodes with @id can be referenced from anywhere rather than repeated, which is what makes a page’s markup a graph rather than a tree.

@graph holds an array of nodes at the top level. Use it when one document describes several entities, for example an Organization, a WebSite, and an Article on the same page.

@value, @type, and @language together express a typed or tagged literal. {"@value": "1867-11-07", "@type": "http://www.w3.org/2001/XMLSchema#date"} is a date rather than a string; {"@value": "Curie", "@language": "fr"} is a French label.

@list and @set control ordering. RDF treats multiple values as an unordered set by default, so @set mostly documents intent. @list produces an ordered RDF collection, which matters for ranked steps or authorship order.

Two more are worth knowing. @reverse expresses a relationship backwards, so a document about a blog can list its posts while the emitted triple points from post to blog. @nest, added in 1.1, groups keys under a wrapper key for readability without that wrapper becoming a property in the graph.

Expanded, compacted, flattened, and framed

The same graph can be written several ways, and JSON-LD defines algorithms that convert between them.

Expanded form drops the context and writes every key and value in full IRI form, with all values as arrays of value objects. It is verbose and predictable, which makes it the right input for diffing and for conversion to other RDF syntaxes.

Compacted form applies a context to shorten IRIs back to terms and collapse single-value arrays. This is the form humans write and the form that belongs on a web page.

Flattened form labels every node, hoists them into a single @graph array, and replaces nesting with @id references. It suits code that looks up nodes by identifier rather than walking a tree.

Framing is the query-shaped operation. A frame is a JSON-LD document describing the shape wanted, and the algorithm reshapes the input graph to match it, embedding referenced nodes where the frame asks for them. It is how an API guarantees a consistent response shape from data stored as a graph.

For everyday structured data, only compaction matters; the rest apply when JSON-LD is processed as RDF.

Embedding JSON-LD in HTML

The standard mechanism is a script tag with the media type application/ld+json:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Organization",
  "name": "Example Corp",
  "url": "https://example.com/"
}
</script>

Because the browser does not execute the block, it can sit in <head> or <body> without affecting rendering. Google Search Central recommends JSON-LD over Microdata and RDFa for a practical reason: the markup is one contiguous block, decoupled from the HTML around it, so a template change cannot silently break it and a content edit cannot orphan half an entity. Attribute formats interleave data with presentation, and every audit means reading the DOM. The comparison is worked through in JSON-LD vs Microdata vs RDFa.

Google does read JSON-LD that JavaScript inserts, since it renders pages before extracting structured data, but that depends on the script executing successfully during the render. Server-side rendering is the safer default.

Injecting it in real stacks

In Next.js, render the block as a script element from page data. Stringify the object rather than hand-writing the JSON, so it stays in sync with the content:

export default function ProductPage({ product }) {
  const jsonLd = {
    "@context": "https://schema.org",
    "@type": "Product",
    name: product.name,
    sku: product.sku,
    offers: {
      "@type": "Offer",
      price: product.price,
      priceCurrency: "USD",
      availability: "https://schema.org/InStock"
    }
  };
  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <h1>{product.name}</h1>
    </>
  );
}

In Google Tag Manager, a Custom HTML tag containing the script block will inject markup on pages where no template access exists. Treat it as a stopgap: the tag fires client-side, values usually come from data-layer variables that may not be populated on first paint, and the markup lives outside the codebase where nobody reviewing the template will find it.

In WordPress, most SEO plugins already emit a JSON-LD graph covering Organization, WebSite, WebPage, and Article. Adding a second hand-rolled block for the same entity produces duplicate and sometimes conflicting statements. Extend the plugin’s graph through its filters, or disable its output for the type being replaced.

Nesting versus referencing by @id

A nested object creates a node inline. A reference names a node defined elsewhere. Both produce valid graphs, and the choice is about identity.

Nest when the child has no independent existence and no URL of its own: a PostalAddress, an Offer, a Rating. Reference when the same entity appears in more than one place, or has a canonical URL other pages should also use. An Article’s publisher is the clearest case, since the publishing organization is one entity across the whole site:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://example.com/#organization",
      "name": "Example Corp",
      "url": "https://example.com/"
    },
    {
      "@type": "Article",
      "@id": "https://example.com/blog/post/#article",
      "headline": "How entity resolution works",
      "publisher": { "@id": "https://example.com/#organization" }
    }
  ]
}

Repeating the full Organization object on every article instead risks creating one entity per page, and any inconsistency between copies is a contradiction to resolve. Fragment identifiers such as #organization are a common convention for nodes that are not themselves web pages.

Typed and language-tagged values

When precision matters, use a value object rather than a bare string:

{
  "@context": "https://schema.org",
  "@type": "Event",
  "name": [
    { "@value": "Paris Book Fair", "@language": "en" },
    { "@value": "Salon du livre de Paris", "@language": "fr" }
  ],
  "startDate": { "@value": "2026-03-20", "@type": "http://www.w3.org/2001/XMLSchema#date" }
}

Language tags matter for multilingual sites and for any graph that will be merged with Wikidata, where nearly every label is tagged. Datatypes matter to validators and to consumers comparing values. Dates should be ISO 8601 (2026-03-20, 2026-03-20T09:00:00-05:00) whether or not the value is wrapped, because most consumers parse strictly.

Copy-paste examples

Each block below is minimal and valid. Replace the URLs and values.

Organization

{
  "@context": "https://schema.org",
  "@type": "Organization",
  "@id": "https://example.com/#organization",
  "name": "Example Corp",
  "url": "https://example.com/",
  "logo": "https://example.com/logo.png",
  "sameAs": [
    "https://www.wikidata.org/wiki/Q00000",
    "https://www.linkedin.com/company/example-corp/"
  ],
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "100 Main Street",
    "addressLocality": "Gainesville",
    "addressRegion": "FL",
    "postalCode": "32601",
    "addressCountry": "US"
  }
}

Person

{
  "@context": "https://schema.org",
  "@type": "Person",
  "@id": "https://example.com/authors/jane-doe/#person",
  "name": "Jane Doe",
  "url": "https://example.com/authors/jane-doe/",
  "jobTitle": "Data Engineer",
  "worksFor": { "@id": "https://example.com/#organization" },
  "sameAs": ["https://orcid.org/0000-0000-0000-0000"]
}

Article

{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "How entity resolution works",
  "datePublished": "2026-02-11",
  "dateModified": "2026-09-08",
  "author": { "@id": "https://example.com/authors/jane-doe/#person" },
  "publisher": { "@id": "https://example.com/#organization" },
  "image": "https://example.com/images/entity-resolution.png",
  "mainEntityOfPage": "https://example.com/blog/entity-resolution/"
}

FAQPage

{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Does JSON-LD have to be in the head?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "No. A script tag with type application/ld+json is valid anywhere in the document."
      }
    },
    {
      "@type": "Question",
      "name": "Can one page have several JSON-LD blocks?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. Consumers merge all blocks on the page into one graph."
      }
    }
  ]
}

Product with an Offer

{
  "@context": "https://schema.org",
  "@type": "Product",
  "name": "Field Notebook, A5",
  "sku": "FN-A5-001",
  "description": "Hardcover A5 notebook with dot grid pages.",
  "image": "https://example.com/images/fn-a5.jpg",
  "brand": { "@type": "Brand", "name": "Example Corp" },
  "offers": {
    "@type": "Offer",
    "url": "https://example.com/products/fn-a5/",
    "price": "18.00",
    "priceCurrency": "USD",
    "availability": "https://schema.org/InStock",
    "itemCondition": "https://schema.org/NewCondition"
  }
}

BreadcrumbList

{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://example.com/" },
    { "@type": "ListItem", "position": 2, "name": "Stationery", "item": "https://example.com/stationery/" },
    { "@type": "ListItem", "position": 3, "name": "Field Notebook, A5", "item": "https://example.com/products/fn-a5/" }
  ]
}

Validating your JSON-LD

Three tools answer different questions, and all three are worth running.

The Rich Results Test (Google) says whether Google can extract the markup and whether it qualifies for a rich result. It renders the page first, so it catches JavaScript-injected markup. It reports only on types Google supports, so silence about a property does not mean the property is wrong.

The Schema Markup Validator (schema.org) checks the markup against the full schema.org vocabulary rather than Google’s subset, catching misspelled properties and types used outside their expected domain.

The JSON-LD Playground covers the JSON-LD layer itself, expanding, compacting, flattening, and framing a document and showing the resulting RDF triples. Wider coverage is in structured data testing tools.

For developers

Treat JSON-LD as an RDF serialization, not a bespoke format. jsonld.js implements the full 1.1 algorithm set in JavaScript, PyLD does the same in Python, and rdflib parses JSON-LD into a graph object alongside Turtle and N-Triples, so a document published for search engines can be queried with SPARQL with no conversion step.

Two patterns pay off. First, generate markup from the same objects that render the page, so the two cannot drift. Second, use framing when serving graph data through an API: store normalized nodes, define a frame per endpoint, and clients get a stable shape while the underlying graph stays free to change.

Context resolution is a network call, so production processors should cache remote contexts or load a bundled local copy. A fetch failure will otherwise fail the whole operation.

For SEOs

Not every property in a valid block does the same job. Some are required or recommended for a rich result and are checked against the visible page: price and availability in an Offer, name and acceptedAnswer in a Question, position and name in a breadcrumb. Others describe the entity for graph-building purposes and will never produce a visual change: sameAs, founder, foundingDate, knowsAbout, parentOrganization. Both matter, for different reasons, and expecting a rich result from the second group is the usual source of disappointment.

@id and sameAs are the entity consistency tools. Give each important entity one stable @id and reuse it across every page and block that mentions it, then point sameAs at identifiers other systems already use (Wikidata, Wikipedia, official profiles, ORCID for people). That gives a search engine a route from the page’s markup to an entity it already holds. See sameAs schema, organization schema, and entity SEO.

The most frequent audit findings are markup describing content that is not on the page, a Product block with no offers, review markup for reviews the site did not collect, and two plugins each emitting an Organization node with slightly different names.

Common mistakes

Omitting @context leaves keys unresolvable, so consumers see a JSON blob and no entity. A mistyped context fails the same way.

Wrong nesting is next. Putting price directly on a Product instead of inside an Offer, or a PostalAddress‘s fields directly on an Organization, produces properties that do not exist on that type. Check the type’s page on schema.org for where a property belongs.

Markup that contradicts the visible page is a policy problem as well as a data problem. Prices, ratings, availability, and FAQ answers must match what a visitor sees.

Invalid dates break silently. "03/20/2026" and "March 20, 2026" are not ISO 8601 and will be rejected or ignored.

Escaping errors inside script tags are the last category. A literal </script> sequence in a string value terminates the block early, so escape it as <\/script>. Raw newlines inside strings and smart quotes pasted from a word processor invalidate the JSON outright. Serializing with a real JSON library rather than string concatenation avoids all of these.

Related pages

FAQ

What is JSON-LD used for?

JSON-LD is used to publish machine-readable data on web pages and to exchange linked data between systems. On the web its main use is schema.org structured data inside a script tag with type application/ld+json, which search engines read to identify entities and generate rich results. Outside search, it serves as an RDF syntax for APIs and knowledge graph pipelines.

Where should JSON-LD go on a page?

Anywhere in the HTML document. A script tag with type application/ld+json is valid in both head and body, and Google states the placement does not matter. Server-rendering it is safer than injecting it with JavaScript, since rendered-only markup depends on the script executing during the crawler’s render pass.

Is JSON-LD better than Microdata?

For most sites, yes, but not because it means anything different. All three schema.org formats expand to the same triples. JSON-LD keeps the data in one block separate from the HTML, which makes it easier to generate, audit, and change without touching templates, and Google recommends it. Microdata still fits cases where markup must stay attached to editor-managed content.

Can a page have more than one JSON-LD block?

Yes. Consumers merge every block on the page into a single graph, so splitting markup across blocks is fine and is often cleaner than one large document. Nodes that share an @id across blocks are treated as the same entity, which is how a per-page Article block can reference a site-wide Organization node.

Sources and further reading

  • W3C, JSON-LD 1.1: https://www.w3.org/TR/json-ld11/
  • W3C, JSON-LD 1.1 Processing Algorithms and API: https://www.w3.org/TR/json-ld11-api/
  • W3C, JSON-LD 1.1 Framing: https://www.w3.org/TR/json-ld11-framing/
  • Google Search Central, Introduction to structured data markup: https://developers.google.com/search/docs/appearance/structured-data/intro-structured-data
  • schema.org, Getting started: https://schema.org/docs/gs.html
  • JSON-LD Playground: https://json-ld.org/playground/
  • jsonld.js: https://github.com/digitalbazaar/jsonld.js