CLI tool
google/schema-dts avatar
google/schema-dts

schema-dts: making the compiler police your JSON-LD

JSON-LD TypeScript types for Schema.org vocabulary

1,239 stars57 forksTypeScriptApache-2.0

At a glance

What is it?
Google publishes TypeScript definitions for the Schema.org vocabulary as a discriminated union per type, plus a generator for pinning a specific schema layer. The interesting parts are the leaf types and the action constraints.
Who is it for?
schema-dts earns its place by moving a large class of silent mistakes into the type checker: a misspelled property, a number where a string belongs, an unknown type name all become compile errors rather than something a rich-results test rejects weeks later.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 36 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the package types and what it leaves out

schema-dts is a set of TypeScript definitions for the Schema.org vocabulary in JSON-LD form. The approach is to expose each Schema.org type as a complete discriminated union rather than a loose interface with optional properties, which is what makes autocomplete useful and validation strict. If you have ever written structured data by holding an object literal and hoping the property names were right, this is the package that turns that into something the compiler checks.

The repository publishes two npm packages and it is worth keeping them straight. `schema-dts` is the pre-packaged typings for the latest Schema.org release. `schema-dts-gen` is the command line tool that generates TypeScript files for a specific Schema version and layer, which is what you reach for when the default bundle is not what you need.

One detail worth checking before you rely on it: the README describes the pre-packaged package as excluding the Pending layer and other non-core layers. That sits oddly against the v1.1.2 release notes, which describe switching back to include all layers including Pending after it had regressed in earlier releases. The likeliest explanation is that the bundle narrowed again after 1.1.2, but the two statements are far enough apart in time that if Pending vocabulary matters to you, generate it yourself with `schema-dts-gen` rather than assuming either answer.

The README also carries an explicit line saying this is not an officially supported Google product. The license is Apache-2.0, the repository shows 1,239 stars, 57 forks, and 14 open issues on `main`, and the last recorded push is dated 2026-09-01, so the vocabulary is still being tracked.

Installing the typings and adding a context to an object

The package is on npm under the name `schema-dts`, and installation is a dev dependency in the documented form:

bash
npm install --save-dev schema-dts

The dev dependency placement is a leftover from an older constraint. Release v1.1.5 removed TypeScript as a peer dependency specifically so that people could install the package as a regular dependency instead, and if your application emits JSON-LD at runtime rather than only building it, a dev dependency is the wrong place for it.

Once installed, the common first move is `WithContext`. A bare Schema.org object does not accept a `@context` property in the type definitions, which is defensible in spirit since most objects in a document are not the root, but inconvenient when you are declaring the root. Wrapping the union in `WithContext` opens the property up:

ts
import type {Person, WithContext} from 'schema-dts';

const p: WithContext<Person> = {
  '@context': 'https://schema.org',
  '@type': 'Person',
  name: 'Eve',
  affiliation: {
    '@type': 'School',
    name: 'Nice School',
  },
};

`affiliation` here is a nested inline object with its own `@type`, and the definitions accept that because the property is typed as a union of the types Schema.org permits there. That is the whole value proposition in one snippet: write `affilition` instead of `affiliation`, or assign a bare string where the vocabulary expects a `School`, and the build fails before the JSON ever ships.

Why every type is exported twice as a union and a leaf

The most useful structural decision in this package is that each Schema.org type comes in two forms. The union alias, such as `Person`, spans the type's subtypes and also allows a plain string, because JSON-LD lets you reference a node by id instead of inlining it. The leaf interface, such as `PersonLeaf`, describes exactly one concrete `@type` with no id-reference escape hatch.

That distinction matters when you want to implement a type rather than write a literal. A union alias cannot appear in an `implements` clause, so a class has nothing to attach to. The leaf interface can:

ts
import type {PersonLeaf} from 'schema-dts';

class Employee implements PersonLeaf {
  readonly '@type' = 'Person';
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}

The guidance in the README is to reach for the leaf interface whenever you need one concrete shape to implement or extend, and to use the union when you are annotating a variable. Leaf types arrived as a documented feature in release v2.0.0, so code written against the 1.x line will need updating before it can use them.

Leaf types also serve the multi-type case. Some Schema.org objects legitimately carry more than one concrete `@type`, and for those the package exports a `MergeLeafTypes` helper to combine individual leaves:

ts
import type {
  MergeLeafTypes,
  ProductLeaf,
  SoftwareApplicationLeaf,
  WithContext,
} from 'schema-dts';

const product: WithContext<
  MergeLeafTypes<[ProductLeaf, SoftwareApplicationLeaf]>
> = {
  '@context': 'https://schema.org',
  '@type': ['Product', 'SoftwareApplication'],
  name: 'My App',
  operatingSystem: 'Any',
};

It expects concrete leaves, not union aliases, which is the one rule that trips people up: passing `Product` where `ProductLeaf` was asked for will not compile, and the error only makes sense once you know the distinction.

Graphs, @id, and referencing a node without repeating it

JSON-LD supports a `@graph` structure where nodes have richer interconnections than a single tree allows, and schema-dts exposes this through the `Graph` type. Any node can carry an `@id`, and any other node can then reference it with an id stub, an object containing only `@id` and nothing else. That is how you describe an author, the page they wrote, and the site that page belongs to without repeating the author twice:

ts
import type {Graph} from 'schema-dts';

const graph: Graph = {
  '@context': 'https://schema.org',
  '@graph': [
    {
      '@type': 'Person',
      '@id': 'https://my.site/#alyssa',
      name: 'Alyssa P. Hacker',
      hasOccupation: {'@type': 'Occupation'},
      mainEntityOfPage: {'@id': 'https://my.site/about/#page'},

The README's fuller example continues with an `AboutPage` and a `WebPage`, each with its own `@id`, using inline nested objects only for things referenced by exactly one parent, such as the `Occupation` node, and id stubs everywhere else. That split between inline and referenced is a modeling decision rather than a typing one, and the example settles on a reasonable default: promote a node to its own `@id` only when something actually points at it.

For most sites the simpler path is a set of independent `WithContext<T>` literals rather than one graph, and nothing in the types pushes you toward `Graph` unless you need genuine cross-references. A `WebSite` with an `Organization` and a `WebPage` is three objects and no graph at all.

Action input and output constraints with WithActionConstraints

Schema.org actions carry extra information describing what goes in and what comes out, expressed in the vocabulary as `-input` and `-output` suffixes on property names. `query-input` on a `SearchAction` is the case almost everyone meets, since it is what tells a search engine which query parameter holds the search term. schema-dts models these with `WithActionConstraints`, applied to whichever action type you are annotating:

ts
import type {SearchAction, WithActionConstraints} from 'schema-dts';

const potentialAction: WithActionConstraints<SearchAction> = {
  '@type': 'SearchAction',
  'query-input': 'required name=search_term_string',
  // ...
};

The wrinkle is that the constraint does not propagate to the containing type on its own, because `potentialAction` on a `WebSite` is typed as `SearchAction` without the constraint layer. The fix is a cast at the assignment site:

ts
import type {SearchAction, WebSite, WithActionConstraints} from 'schema-dts';

const website: WebSite = {
  '@type': 'WebSite',
  potentialAction: {
    '@type': 'SearchAction',
    'query-input': 'required name=search_term_string',
  } as WithActionConstraints<SearchAction>,
};

Input and output constraints arrived in release v2.0.0 alongside Schema.org v30 support, so this is a 2.x feature and not something backported. The cast is a small wart in an otherwise fully typed API, and it is the sort of thing worth confining to one helper function rather than scattering through a codebase.

The monorepo behind the two packages and how the releases land

The repository is a private monorepo named simply `monorepo`, with the two packages under `packages/` and a shared `tsconfig-base.json` at the root. The root `package.json` is where the tooling story lives. TypeScript is pinned at `^5.9.3` with `ts-jest` at `^29.4.6` for tests, ESLint at `^10.1.0` alongside `typescript-eslint` at `^8.57.1` and an `eslint-plugin-jsdoc` at `^62.8.0`, Prettier at `^3.8.1`, and Jest at `^30.3.0`. There is a `.prettierrc.json`, an `.eslintignore`, and separate `lint:prettier` and `lint:eslint` scripts behind a combined `lint`.

Two details in there are worth noticing. The `typescript-eslint` overrides block pins TypeScript to the same version as the root, a standard way to stop the linter resolving a second copy of the compiler. And the `engines` block asks for Node `>=14.0.0` with `npm >=7.0.0` and `engineStrict` set to true, which is a much older floor than the dev dependencies imply, so read it as untested rather than supported.

Working in the repository rather than consuming the package, `npm run build` fans out across workspaces and `npm run clean` removes the built output through `rimraf` before the build regenerates it. For consumers none of this matters, and that is the right relationship. What matters is the release cadence. Version 2.0.0 on 2026-03-23 brought Schema.org v30 and the breaking changes, including non-recursive `Role` typings and `Quantity` becoming a core DataType, which together make some assignments that used to compile, and were probably wrong anyway, stop compiling. Version 1.1.5 on 2025-03-01 brought Schema.org v28 and the peer dependency change, and version 1.1.2 on 2023-02-24 brought v15 plus clearer TypeScript output that substituted plain `type` for unnecessary `declare type`. A version two majors behind the vocabulary is two Schema.org releases behind, so check your pin before assuming a property you can see on schema.org today is available to you.

Editorial conclusion

schema-dts earns its place by moving a large class of silent mistakes into the type checker: a misspelled property, a number where a string belongs, an unknown type name all become compile errors rather than something a rich-results test rejects weeks later. The generator, `schema-dts-gen`, is the half that matters if you need the Pending layer or a pinned older vocabulary, and the pre-packaged `schema-dts` package is the half that matters if you just want the current release. Install it as a regular dependency rather than a dev dependency if you are not on version 1.1.5 or later, since earlier releases treated TypeScript as a peer dependency and that split cleanly. Read `examples.md` for the patterns the README only starts to show, because it cuts off partway through the Organization and WebSite example.

Frequently asked questions

What is schema-dts used for?

It provides TypeScript definitions for the Schema.org vocabulary in JSON-LD, exposed as discriminated type unions so that misspelled properties or wrong value types become compile errors instead of runtime surprises.

Should schema-dts be a dependency or a devDependency?

As a regular dependency if your code builds or emits JSON-LD at runtime. The documented install uses the dev dependency form, but release v1.1.5 dropped TypeScript as a peer dependency precisely so a regular dependency install works cleanly.

What is the difference between a union type and a leaf type in schema-dts?

The union alias such as `Person` covers subtypes and also allows a string id reference, while the leaf interface such as `PersonLeaf` describes exactly one concrete `@type`. Use the leaf when implementing a class, since a union cannot appear in an implements clause.

How do I add a @context property to a schema-dts object?

Wrap the type in `WithContext`, for example `WithContext<Person>`, which opens the object up to accept `@context` alongside the Schema.org properties.

How do schema-dts and schema-dts-gen differ?

`schema-dts` ships pre-packaged typings for the latest Schema.org release, while `schema-dts-gen` is a command line tool that generates TypeScript files for a specific Schema version and layer.

How do I type a SearchAction query-input property?

Use `WithActionConstraints<SearchAction>` on the action object, or cast the nested action at the assignment site when it sits inside another type such as `WebSite`. This support arrived in release v2.0.0.

Official sources

  1. google/schema-dts on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/google-schema-dts.svg)](https://hysenlabs.com/projects/google-schema-dts)