Open-source project
RehanSaeed/Schema.NET avatar
RehanSaeed/Schema.NET

Schema.NET and the case for typed classes over JSON-LD dictionaries

Schema.org objects turned into strongly typed C# POCO classes for use in .NET. All classes can be serialized into JSON/JSON-LD and XML, typically used to represent structured data in the head section of html page.

689 stars88 forksC#MIT

At a glance

What is it?
Schema.NET turns the schema.org vocabulary into strongly typed C# plain old CLR objects that serialise to JSON-LD or XML, and its real contribution is making a multi-valued, multi-typed property assignable in C# without a dictionary or a cast. Its weakness is that it is a fixed version of a vocabulary that keeps moving, with a second package for the parts not yet settled.
Who is it for?
Schema.NET suits a .NET team emitting structured data for a search engine or a sharing target, because the type system removes the dictionary and cast work that schema.org properties normally impose, and the single-value, list and mixed-type forms all compile.
Can I use it commercially?
Yes. MIT 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 9 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

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

Editorial analysis

Three lines of C# become a JSON-LD block

The simplest example in the project shows the whole proposition. A WebSite object is constructed, three properties are set, and the result is a string:

C#
var website = new WebSite()
{
    AlternateName = "An Alternative Name",
    Name = "Your Site Name",
    Url = new Uri("https://example.com")
};
var jsonLd = website.ToString();

What comes out is JSON-LD, with the context and the type added automatically:

JSON
{
    "@context":"https://schema.org",
    "@type":"WebSite",
    "alternateName":"An Alternative Name",
    "name":"Your Site Name",
    "url":"https://example.com"
}

The important detail is what the caller did not have to write. The @context is not in the source, the @type is not in the source, and the property names are transformed from the Pascal case of the C# properties to the lower case that schema.org uses. The class knows its own type name and the serialiser applies the conventions.

That is the value proposition of the library in one example. schema.org is a vocabulary, and a vocabulary used through a dictionary means every property name is a string literal and every value is an object you cast at the point of use. Here the compiler checks both.

The primary language is C#, the licence is MIT, and the package is published to NuGet as well as to an Azure Artifacts feed.

The one property that breaks every other implementation

schema.org properties can have a single value or an array of multiple values, and a property can have multiple types. An Address property, for example, could be a string or a PostalAddress, and a PostalAddress has its own properties such as StreetAddress or PostalCode, which break an address into its constituent parts.

That combination is where a hand-rolled implementation falls apart, because a single property slot has to hold a string, a PostalAddress, a list of strings, or a list of PostalAddresses, and C# will not let you assign those to the same member without a conversion.

Schema.NET solves it with implicit conversion operators in four types, OneOrMany<T>, Values<T1, T2>, Values<T1, T2, T3> and Values<T1, T2, T3, T4>, and the documentation notes that all of them are structs for best performance. So the same property accepts all of these forms:

C#
// Single string address
var organization = new Organization()
{
    Address = "123 Old Kent Road E10 6RL"
};

// Multiple string addresses
var organization = new Organization()
{
    Address = new List<string>()
    { 
        "123 Old Kent Road E10 6RL",
        "456 Finsbury Park Road SW1 2JS"
    }
};

And a structured value works the same way:

C#
// Single PostalAddress address
var organization = new Organization()
{
    Address = new PostalAddress()
    {
        StreetAddress = "123 Old Kent Road",
        PostalCode = "E10 6RL"
    }
};

A list of structured values works too, and so does a genuinely mixed list:

C#
// Mixed Author types
var book = new Book()
{
    Author = new List<object>()
    {
        new Organization() { Name = "Penguin" },
        new Person() { Name = "J.D. Salinger" }
    }
};

And the result can be taken apart again, which is the part that makes the mixed list usable rather than merely assignable:

C#
// Deconstruct a property containing mixed types
if (book.Author.HasValue)
{
    var (organisations, people) = book.Author.Value;
}

A deconstruction into two typed locals from a list that held two different types is a genuine convenience, and it is the reason to use a library like this rather than a hand-rolled model.

Where the output goes, and the escaping trap in it

The intended use is the head section of an HTML page, where structured data tells search engines how to display richer information in results. The example in the documentation shows the script element and its MIME type:

The example in the documentation shows a script element with the application/ld+json MIME type containing an Organization object, with a contactPoint that is itself a ContactPoint carrying a telephone number and a contact type of Customer service, and the url and name of the organisation.

And then there is the security notice, which is the part of the documentation worth reading twice. When serialising for a script tag you should use the alternate ToHtmlEscapedString method, to avoid exposing yourself to a cross-site scripting vulnerability if some of the properties in your schema have been set from untrusted sources.

That is a real and easy mistake. ToString produces the JSON-LD, and dropping it into a script element raw is a cross-site scripting hole the moment any property value comes from user input or from a database someone else can write to. The alternate method exists for exactly that case.

In an ASP.NET MVC project the usage looks like this:

HTML
<script type="application/ld+json">
    @Html.Raw(Model.Schema.ToHtmlEscapedString())
</script>

Note that it is still Html.Raw, because the method escapes the values for the script context rather than HTML encoding the whole block, and encoding the whole block would break the JSON. That subtlety is exactly why the method has to exist rather than the caller doing it by hand.

There is a second use case in the documentation, Windows UWP sharing, where apps share data using schema.org classes, with a link to an example showing how to share metadata about a book.

A fixed version of a vocabulary, and the second package that covers the gap

The hard problem with any schema.org library is that the vocabulary itself is not finished.

There are many pending types on schema.org which are not yet fully formed and ready for production, and for those the project publishes a second NuGet package, Schema.NET.Pending, which contains all the released schema types as well as all the pending types. So the choice is between a package that contains only the settled vocabulary and one that contains both.

This is the right structure for the problem, and it is worth understanding why it exists rather than treating it as an oddity. schema.org adds types over time, and many of them arrive before their properties are stable. A library that waited for stability would be years behind; a library that included everything would expose callers to properties that change shape. Splitting the packages lets the stable package stay stable while the pending package tracks the frontier.

The release history shows how the split is expressed in versions. The listed releases are 11.0.1 from 2022-02-22, 12.0.0 from 2023-05-10 and 13.0.0 from 2023-12-17. The major version bumps are the natural place to make a breaking change to a type shape, and the last push to the repository was on 2026-10-14, so work continues on the generated code even though the newest tagged release is from 2023.

The repository layout is consistent with a generated codebase. There is a Source/ directory, a Data/ directory that presumably holds the vocabulary description, Tests/, Tools/, a Benchmarks/ directory, a Data/ folder, a Schema.NET.sln, and build files including a Cake build script and Azure Pipelines plus AppVeyor configuration. There is also a Directory.Build.props and Directory.Build.targets, a global.json pinning the SDK, a renovate.json for dependency updates, and a Key.snk for strong naming, which matters for a library that other teams reference from signed builds.

Where Schema.NET is the wrong choice

The first case is a project that only needs a couple of properties. If you are emitting one Organization block with a name and a URL, a generated class hierarchy of several thousand types and an implicit conversion machinery is more than the problem deserves, and the plain JSON-LD literal is clearer.

The second is a project that needs the latest schema.org additions today. The stable package stops at whatever was current when it was cut, and the pending package brings the unsettled types with it, which is the trade the library makes rather than solves. If your property appeared last month and is not in either package, you are waiting on a release.

The third is a team that would rather own its JSON. Some projects prefer a serialisation library with a source generator over a large pre-generated model, and that preference is about build times and assembly size as much as about style.

The fourth is any output path other than a script tag. The library is oriented toward JSON-LD and XML for structured data, and while the classes are plain objects you could use elsewhere, the type hierarchy exists to solve one problem, so a project using schema.org classes for an internal API would be paying for a vocabulary it only partly needs.

One more practical note for the .NET side: the repository uses a strong-naming key and pins the SDK through global.json, so building it locally needs the expected toolchain, and the assembly is a dependency you will be referencing at a fixed major version.

Editorial conclusion

Schema.NET suits a .NET team emitting structured data for a search engine or a sharing target, because the type system removes the dictionary and cast work that schema.org properties normally impose, and the single-value, list and mixed-type forms all compile. It does not suit a project that needs the very latest schema.org additions, since the released package lags and the pending package exists precisely because of that, and it does not suit a team that wants to hand-write the JSON-LD, because the library assumes you describe the graph in C# first. Before you adopt it, read the escaping note and use ToHtmlEscapedString rather than ToString when the output goes into a script tag, and note that the last push was on 2026-10-14 while the newest tagged release is 13.0.0 from 2023-12-17.

Frequently asked questions

What is Schema.NET?

It is an MIT licensed library that turns schema.org objects into strongly typed C# plain old CLR classes for use in .NET. Every class can be serialised into JSON, JSON-LD and XML, and the typical use is structured data in the head section of an HTML page.

How do I handle a schema.org property that can hold one value or many?

The library uses implicit conversion operators in OneOrMany<T>, Values<T1, T2>, Values<T1, T2, T3> and Values<T1, T2, T3, T4>, all implemented as structs for performance. That lets you assign a single string, a list of strings, a single PostalAddress or a list of PostalAddress to the same property.

How do I avoid an XSS vulnerability when writing schema.org JSON-LD to a script tag?

Use the ToHtmlEscapedString method rather than ToString when the output goes into a script element, because a property set from an untrusted source can otherwise inject script. In ASP.NET MVC the documented usage is @Html.Raw(Model.Schema.ToHtmlEscapedString()) inside a script tag with the application/ld+json type.

What is the Schema.NET.Pending package for?

schema.org has many pending types that are not yet fully formed and ready for production. The Schema.NET.Pending package contains all the released schema types plus all the pending ones, so you can use types that are still settling without waiting for the stable package to catch up.

What is the latest Schema.NET release?

The listed releases are 13.0.0 from 2023-12-17, 12.0.0 from 2023-05-10 and 11.0.1 from 2022-02-22, and the last push to the repository was on 2026-10-14. The package is published on NuGet and on an Azure Artifacts feed, and it is also usable for Windows UWP sharing of metadata.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. RehanSaeed/Schema.NET on GitHub
  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/rehansaeed-schema-net.svg)](https://hysenlabs.com/projects/rehansaeed-schema-net)

Was this useful?