Zurück

04/11/2025

JSON mit Schema validieren

Im letzten Beitrag habe ich gezeigt, wie sich in C# mit Newtonsoft.Json Klassen zu JSON serialisieren und wieder zurück deserialisieren lassen. Dabei kann allerdings auch einiges schiefgehen: Newtonsoft deserialisiert munter alles, was halbwegs passt, und ignoriert unbekannte Felder einfach. Es gibt also keine Garantie, dass das eingehende JSON wirklich die erwartete Struktur hat. Genau hier setzt ein JSON-Schema an. In diesem Beitrag zeige ich, wie sich passend zu unseren Klassen mit NJsonSchema ein Schema generieren lässt.


Warum überhaupt ein Schema?

Ein JSON-Schema beschreibt, wie ein gültiges JSON-Dokument aussehen muss. Welche Felder erwartet werden, welche Typen sie haben und was davon Pflicht ist. Damit können wir eingehende Daten prüfen, bevor wir sie deserialisieren, und so sicherstellen, dass wirklich nur das Richtige durchkommt. Das ist besonders wertvoll, wenn die Daten von außen kommen, etwa von einer fremden API oder aus einer Datei, deren Inhalt wir nicht kontrollieren.

Statt das Schema von Hand zu schreiben und bei jeder Änderung der Klasse mühsam nachzupflegen, generieren wir es einfach direkt aus unseren bestehenden C#-Klassen.


Warum NJsonSchema und nicht Newtonsoft.Json.Schema?

Da wir im letzten Beitrag mit Newtonsoft gearbeitet haben, liegt der Gedanke nahe, auch fürs Schema bei Newtonsoft zu bleiben. Es gibt schließlich Newtonsoft.Json.Schema. Bei genauerem Hinsehen ist das aber ein eigenständiges, kommerzielles Produkt und nicht Teil des kostenlosen Newtonsoft.Json.

  • Lizenz: Newtonsoft.Json.Schema steht unter AGPL bzw. einer kostenpflichtigen Lizenz. Die Gratis-Variante ist auf 1000 Validierungen pro Stunde beschränkt. Für ernsthafte oder kommerzielle Nutzung führt kein Weg an einer kostenpflichtigen Lizenz vorbei.
  • NJsonSchema dagegen steht unter der MIT-Lizenz, ist also komplett frei, auch für den kommerziellen Einsatz, und ohne irgendwelche Limits.

NJsonSchema ist zudem extrem verbreitet, es bildet zum Beispiel die Grundlage für NSwag. Es nutzt unter der Haube weiterhin Json.NET zum Lesen und Schreiben, fügt sich also nahtlos in das ein, was wir bereits kennen.


NJsonSchema installieren

Wie gewohnt fügen wir zuerst das Paket über NuGet hinzu. Da wir aus dem Newtonsoft-Beitrag kommen, nehmen wir nicht das nackte NJsonSchema, sondern NJsonSchema.NewtonsoftJson. Warum, dazu gleich mehr. Das Kernpaket NJsonSchema wird dabei als Abhängigkeit automatisch mitinstalliert.

dotnet add package NJsonSchema.NewtonsoftJson

Anschließend stehen uns die passenden Namespaces zur Verfügung.

using NJsonSchema;
using NJsonSchema.NewtonsoftJson.Generation;

Warum das Newtonsoft-Paket?

Bis Version 10 war NJsonSchema fest an Newtonsoft.Json gekoppelt. Mit Version 11 wurde das aufgespalten: Das Kernpaket NJsonSchema ist seitdem unabhängig vom verwendeten JSON-Serializer, und JsonSchemaGeneratorSettings ist eine abstrakte Basisklasse. Wir müssen also explizit angeben, nach welchen Serialisierungsregeln das Schema erzeugt werden soll. Dafür gibt es zwei Varianten:

  • SystemTextJsonSchemaGeneratorSettings für System.Text.Json (im Kernpaket enthalten)
  • NewtonsoftJsonSchemaGeneratorSettings für Newtonsoft.Json (im Paket NJsonSchema.NewtonsoftJson)

Da wir im letzten Beitrag mit Newtonsoft und dessen Attributen wie [JsonProperty] und [JsonIgnore] gearbeitet haben, brauchen wir die Newtonsoft-Variante. Nur so respektiert das generierte Schema genau diese Attribute. Mit dem reinen NJsonSchema-Paket würde stattdessen über System.Text.Json reflektiert, und unsere Newtonsoft-Attribute würden ignoriert.


Schema aus einer Klasse generieren

Wir nutzen dieselbe User-Klasse aus dem letzten Beitrag.

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public bool EmailVerified { get; set; }
}

Das Generieren des Schemas ist nun schnell erledigt. Über JsonSchema.FromType lassen wir NJsonSchema die Klasse per Reflection analysieren und übergeben dabei die NewtonsoftJsonSchemaGeneratorSettings, damit die Newtonsoft-Regeln greifen. Mit ToJson geben wir das Ergebnis als String aus.

var settings = new NewtonsoftJsonSchemaGeneratorSettings();
 
JsonSchema schema = JsonSchema.FromType<User>(settings);
string schemaJson = schema.ToJson();

Heraus kommt ein vollständiges JSON-Schema, das unsere Klasse exakt beschreibt.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "Id": {
      "type": "integer",
      "format": "int32"
    },
    "Name": {
      "type": "string"
    },
    "Email": {
      "type": "string"
    },
    "EmailVerified": {
      "type": "boolean"
    }
  }
}

NJsonSchema erkennt dabei automatisch die Typen. Aus einem int wird integer mit dem Format int32, aus einem bool wird boolean und so weiter. Auch verschachtelte Objekte und Listen werden korrekt abgebildet, ganz so, wie wir es beim Serialisieren gesehen haben.


JSON gegen das Schema validieren

Jetzt kommt der eigentlich spannende Teil. Mit dem generierten Schema können wir prüfen, ob ein beliebiges JSON überhaupt passt, bevor wir es deserialisieren. Dafür stellt das schema-Objekt die Methode Validate bereit. Sie liefert eine Liste mit Fehlern zurück. Ist diese leer, war die Validierung erfolgreich.

string json = File.ReadAllText("user.json");
 
var errors = schema.Validate(json);
 
if (errors.Count == 0)
{
    User user = JsonConvert.DeserializeObject<User>(json);
    Console.WriteLine("JSON ist gültig.");
}
else
{
    foreach (var error in errors)
    {
        Console.WriteLine($"{error.Path}: {error.Kind}");
    }
}

Erst wenn das JSON die Prüfung besteht, deserialisieren wir es. So fangen wir fehlerhafte Daten ab, bevor sie überhaupt in unser Objekt gelangen.

Nehmen wir an, in der user.json ist EmailVerified versehentlich ein String statt eines Booleans.

{
  "Id": 1,
  "Name": "Valentin",
  "Email": "me@pndl.net",
  "EmailVerified": "ja"
}

Dann meldet uns die Validierung genau das, inklusive des betroffenen Pfads.

EmailVerified: BooleanExpected

Jeder ValidationError enthält neben der Art des Fehlers (Kind) auch den genauen Pfad im Dokument (Path) und die Zeilennummer. Damit lässt sich sehr präzise nachvollziehen, was am eingehenden JSON nicht stimmt.


Bonus: Code aus einem Schema generieren

Der Weg funktioniert übrigens auch in die andere Richtung. Liegt uns bereits ein fertiges JSON-Schema vor, kann NJsonSchema daraus passende C#-Klassen erzeugen. Dafür gibt es das Zusatzpaket NJsonSchema.CodeGeneration.CSharp.

JsonSchema schema = await JsonSchema.FromJsonAsync(schemaJson);
 
var generator = new CSharpGenerator(schema);
string code = generator.GenerateFile();

Das ist enorm praktisch, wenn man zu einer fremden API nur das Schema bekommt und sich die ganzen DTO-Klassen nicht von Hand schreiben möchte. Neben C# unterstützt NJsonSchema dabei auch TypeScript.


Fazit

Mit einem generierten JSON-Schema schließen wir die Lücke, die uns das reine Deserialisieren offen lässt. Wir stellen sicher, dass eingehende Daten wirklich der erwarteten Struktur entsprechen, bevor wir sie weiterverarbeiten. NJsonSchema macht das mit JsonSchema.FromType und Validate sehr leicht, ist dabei frei und ohne Limits nutzbar und integriert sich nahtlos in das Newtonsoft-Ökosystem, mit dem wir ohnehin schon arbeiten. Newtonsoft.Json.Schema bietet zwar Ähnliches, ist durch seine Lizenz und das Validierungslimit für die meisten Projekte aber die deutlich unattraktivere Wahl.

Ich kann nur empfehlen, Schema-Validierung überall dort einzubauen, wo JSON von außen ins eigene System gelangt. Der Aufwand ist gering und erspart einem im oft viel Fehlersuche.