DEV Community

Cover image for Modeling Schema.org JSON-LD in TypeScript: A Story in Four Parts
Eyas
Eyas

Posted on

Modeling Schema.org JSON-LD in TypeScript: A Story in Four Parts

Recently, I published schema-dts, an open source library that models JSON-LD Schema.org in TypeScript. A big reason I wanted to do this project is because I knew some TypeScript type system features, such as discriminated type unions, powerful type inference, nullability checking, and type intersections, present an opportunity to both model what Schema.org-conformant JSON-LD looks like, while also providing ergonomic completions to the developer.

I wrote a series of posts, describing some of the Structured Data concepts that lent themselves well to TypeScript’s type system, and those concepts that didn’t.

1. Modeling Schema.org Class Hierarchy using Structural Typing

Schema.org JSON-LD node objects are always typed (that is, they have a @type property that points to some IRI–a string–describing it). Given a @type you know all the properties that are defined on a particular object. Object types inherit from each other. For example, Thing in Schema.org has a property called name, and Person is a subclass of Thing that defines additional properties such as birthDate, and inherits all the properties of Thing such as name. Thing has other sub-classes, like Organization, with it’s own properties, like logo.

In the first installment, we end up uncovering a recursive TypeScript inheritance hierarchy we can use to model the full complexity of Schema.org class inheritance.

Recursive Hierarchy

2. Schema.org Enumerations in TypeScript

When trying to model Enumerations, we looked at a ton of examples from the Schema.org website to discover that absolute IRIs or @context-relative IRIs are expected to model the value of an enumeration. But we also found out that Enumerations can be arbitrary nodes, and take part in the class hierarchy.

Example of an Enum Hierarchy

3. Schema.org DataType in TypeScript

The Schema.org DataType hierarchy is far richer than TypeScript’s type system can accommodate. In the third installment, we figured out what trade-offs can we make.

4. Class Properties and Special Cases

Properties -- all the stuff that actually lives within a JSON node -- turn out to be more complicated than we thought: they're all optional, they're all repeated, they can supersede one another, and then can subclass one another.

The End Result

The end result is schema-dts itself. We can create programmatic TypeScript definitions that express much of Schema.org. For example, the top-level Thing type in Schema.org can be represented as:

type ThingBase = {
    /** An additional type for the item, typically used for adding more specific types from external vocabularies in microdata syntax. This is a relationship between something and a class that the thing is in. In RDFa syntax, it is better to use the native RDFa syntax - the 'typeof' attribute - for multiple types. Schema.org tools may have only weaker understanding of extra types, in particular those defined externally. */
    "additionalType"?: URL | URL[];
    /** An alias for the item. */
    "alternateName"?: Text | Text[];
    /** A description of the item. */
    "description"?: Text | Text[];
    /** A sub property of description. A short description of the item used to disambiguate from other, similar items. Information from other properties (in particular, name) may be necessary for the description to be useful for disambiguation. */
    "disambiguatingDescription"?: Text | Text[];
    /** The identifier property represents any kind of identifier for any kind of {@link http://schema.org/Thing Thing}, such as ISBNs, GTIN codes, UUIDs etc. Schema.org provides dedicated properties for representing many of these, either as textual strings or as URL (URI) links. See {@link /docs/datamodel.html#identifierBg background notes} for more details. */
    "identifier"?: (PropertyValue | Text | URL) | (PropertyValue | Text | URL)[];
    /** An image of the item. This can be a {@link http://schema.org/URL URL} or a fully described {@link http://schema.org/ImageObject ImageObject}. */
    "image"?: (ImageObject | URL) | (ImageObject | URL)[];
    /** Indicates a page (or other CreativeWork) for which this thing is the main entity being described. See {@link /docs/datamodel.html#mainEntityBackground background notes} for details. */
    "mainEntityOfPage"?: (CreativeWork | URL) | (CreativeWork | URL)[];
    /** The name of the item. */
    "name"?: Text | Text[];
    /** Indicates a potential Action, which describes an idealized action in which this thing would play an 'object' role. */
    "potentialAction"?: Action | Action[];
    /** URL of a reference Web page that unambiguously indicates the item's identity. E.g. the URL of the item's Wikipedia page, Wikidata entry, or official website. */
    "sameAs"?: URL | URL[];
    /** A CreativeWork or Event about this Thing.. */
    "subjectOf"?: (CreativeWork | Event) | (CreativeWork | Event)[];
    /** URL of the item. */
    "url"?: URL | URL[];
};
/** The most generic type of item. */
export type Thing = ({
    "@type": "Thing";
} & ThingBase) | (Action | CreativeWork | Event | Intangible | MedicalEntity | Organization | Person | Place | Product);

View the entire series at https://blog.eyas.sh/tag/schema.org

Oldest comments (2)

Collapse
 
johnsonjo4531 profile image
John Johnson

Thanks so much for this library @Eyas!

Collapse
 
shnydercom profile image
Jonathan Schneider • Edited

I'm really happy I found this library now! I've been working on a lowcode-editor for frontends, with the application state using schema.org-types (at runtime). Since n-to-n relationships seem like the default for properties, and almost anything can relate to any Thing, I admire your work (and will use your library)! :) Thanks for building and sharing this!