Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

Introduction to JSON Schema

4.99/5 (27 votes)
13 Mar 2018CPOL23 min read 56.6K   595  
JSON Schema may be the answer to most problems in dealing with JSON files. We explore JSON Schema from a practical point of view.

Image 1

Contents

  1. Introduction
  2. Background
  3. JSON Schema
    1. Specification v3
    2. Specification v4
    3. Specification v6
    4. Specification v7
    5. Schema Constraints
    6. Visual Studio Code
    7. JSON.NET Schema
  4. The Editor
  5. Using the code
  6. Points of Interest
  7. References
  8. History

Introduction

In this article we'll look into creating an awesome JSON schema editor using WPF. To fully understand the code we need to have some proper introduction to JSON schema. We will see the JSON schema is very useful to put some constraints on a JSON file. Without a schema JSON files could - by definition - contain arbitrary data.

Besides the advantage of putting some boundaries and requirements to JSON files we also have another immediate benefit from creating a JSON schema file: We can help users to understand what options are available and how the structures need to look to be right. Also everything can be described and enhanced with some additional information that may be displayed in some help text.

One of the best parts of JSON schema is that it is full y written in JSON. Hell, there is even a JSON schema that describes how a JSON schema file should look like.

Before going into details of JSON schema we will take a quick look at my own background related to JSON schema files. One of the most important sections will then discuss the differences between the various versions of the JSON schema specification.

Background

Actually, I wrote this article over 1 year ago - but I never found the time to publish it. This will now change. I will also try to update the content accordingly, as at the time of writing v4 was the most current spec. Right now we have v7. The migration from v4 to v6 and the differences to v7 will be explained in future revisions of this article.

Recently I created an application that required a sophisticated condition configuration to run properly. The configuration was supplied in form of a JSON file. The problems I faced immediately were as follows:

  • How to check or validate the input in a flexible way
  • How to inform users about missing or invalid parts
  • How to support multiple versions without being in a dependency hell
  • How to offer an editor that allows creating configuration files without duplicating code

As the interested reader may guess JSON schema solved these problems for me.

What we will do in this article is to have a closer look at JSON schema, it's properties, available libraries, and short comings. Our goal is to write a graphical JSON editor based on a given schema. Since JSON schema is defined in form of a JSON schema we can also use the editor to edit the schema itself.

That being said: Full throttle ahead! Let's explore JSON schema.

JSON Schema

JSON schema is a format that may be used to formalize constraints and requirements to JSON files. We can specify

  • What properties exists
  • Which type(s) can be used
  • If properties are required
  • How the JSON is composed
  • Why the properties are relevant

The official website hosts the specification and some examples. One of the major problems is that there is no single point of information. The JSON schema specification rather consists of different versions with the most used ones being the earlier v3 and the current v4 specification. We'll go into the differences of these two specifications in the next two sections.

Specification v3

The third iteration of the JSON schema specification was a great success. Even today many systems still rely on this version instead of updating to a more recent draft. We'll see that this is for a reason as not all changes may be appreciated by everyone.

First let's start with the fundamentals of JSON schema, i.e., things that work across v3 and v4 and can be considered basic for any JSON schema.

A JSON schema defines the contents of a JSON file. In a recursive manner this definition is also performed in a JSON file. Most notably this definition implies:

  • The used data type
  • The allowed properties (for an object)
  • The allowed items (for an array)
  • The allowed values (for an enumeration, enum)
  • If an item is required or optional
  • The minimum and maximum (for a number)
  • The pattern to follow (for a string)
  • ...

Besides these obvious (and some not so obvious) constraints we may also include some metadata like a description of the field. The description could be helpful when validation fails or when the schema forms the basis for autocomplete in an editor.

A schema may be as trivial as the following code, which just specifies a JSON that should be a single string.

 

JSON
{
  "$schema": "http://json-schema.org/draft-03/schema#",
  "type": "string"
}

The $schema key is not required, however, helps to identify to right version of the schema. In this case we use the specification v3 - later we will refer to v4 by changing the URL to http://json-schema.org/draft-04/schema#.

Placing a description for the string is straight forward:

JSON
{
  "$schema": "http://json-schema.org/draft-03/schema#",
  "description": "This is a really simple JSON file - just enter a string!",
  "type": "string"
}

Of course, most JSON files will be more than a primitive value (i.e., a complex value). So let's see how the definition of an object with some properties may look like.

JSON
{
  "$schema": "http://json-schema.org/draft-03/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "type": "string"
    },
    "bar": {
      "type": "number"
    }
  }
}

The previous JSON schema validates the following JSON snippets:

JavaScript
// valid:
{ }

// valid:
{ "foo": "bar" }

// valid:
{ "bar": 3 }

// valid:
{ "baz": false }

// valid:
{ "foo": "bar", "bar": 4.1, "baz": [] }

However, the upcoming JSON snippets are all invalid:

JavaScript
// invalid:
[]

// invalid:
{ "foo": false }

// invalid:
{ "bar": "foo" }

// invalid:
{ "foo": "bar", "bar": [] }

Important, we still have not seen how we put constraints on the given properties besides the type or specify alternatives. Let's start with the latter. A common scenario is that an item of an array can be one of many types. Here, the type field can be used with an array.

JSON
{
  "$schema": "http://json-schema.org/draft-03/schema#",
  "type": "array",
  "items": {
    "type": [
      { "type": "string" },
      { "type": "number" }
    ]
  }
}

Now let's say that arrays with similar constraints appear more often in our schema. We start with the following example:

JSON
{
  "$schema": "http://json-schema.org/draft-03/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "type": "array",
      "items": {
        "type": [
          { "type": "string" },
          { "type": "number" }
        ]
      }
    },
    "bar": {
      "type": "array",
      "items": {
        "type": [
          { "type": "string" },
          { "type": "number" }
        ]
      }
    }
  }
}

We clearly violate the Don't Repeat Yourself (DRY) principle here. This would have a negative impact on the maintainability of the schema file. Luckily, the JSON schema specification defines a way to declare definitions for being reused by reference.

We now refactor the previous example to use such definitions.

JSON
{
  "$schema": "http://json-schema.org/draft-03/schema#",
  "definitions": {
    "MyArray": {
      "type": "array",
      "items": {
        "type": [
          { "type": "string" },
          { "type": "number" }
        ]
      }
    }
  },
  "type": "object",
  "properties": {
    "foo": {
      "$ref": "#/definitions/MyArray"
    },
    "bar": {
      "$ref": "#/definitions/MyArray"
    }
  }
}

The $ref field puts the given reference in place by bringing in all defined properties of the referenced definition. This is our way of choice to still obey DRY. The beauty of such references lies in the fact that the value is an URI, which may not only refer to local definitions (declared in the same file), but also definitions from other files. These files may be available on the Internet or taken from the local file system (usually we would do this by using a relative path).

Coming back to one of the earlier questions: How do we make certain properties of an object mandatory? The answer is simple: We state that these properties are required! In the specification v3 this looks as follows:

JSON
{
  "$schema": "http://json-schema.org/draft-03/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "type": "string",
      "required": "true"
    },
    "bar": {
      "type": "number"
    }
  }
}

However, thinking about the way to create definitions given earlier, we see that this way of declaring required properties is neither scalable nor easy to decompose. Hence it was changed with the specification v4. Time to continue with the latest spec!

Specification v4

What could possibly be improved? For starters, one of the issues of the previous specification was the modularity was broken. This issue may not be easy to spot directly hence it was released into earlier versions of the specification.

How is the modularity given in a JSON schema? The modules are constructed by individual JSON schema files, where one file references another file. In JSON schema v3 we could not write, e.g., one individual module that could be reused in multiple other files. The reason was that, e.g., properties such as if the module was required by the referring file had to be specified in the target module. Ideally, we would like to specify properties that are only known by the referrer within the referrer's module.

In the specification v4 we have the new required field that can be used for objects. This field expects a list of strings, which denote the different (required) properties of the object. The properties do not need to be defined explicitly (only if we put further constraints, e.g., a certain type, or metadata, e.g., the description) on it.

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "type": "string"
    },
    "bar": {
      "type": "number"
    }
  },
  "required": [
    "foo"
  ]
}

If we think back about JSON Schema v3 we know that we misused the type field with an array to include multiple potential types. However, this approach was not very elegant and also did not provide the flexibility (and expressiveness) that was intended by a JSON schema. As a consequence, the specification v4 does not allow this. Instead, the former example reads:

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "array",
  "items": {
    "oneOf": [
      { "type": "string" },
      { "type": "number" }
    ]
  }
}

The oneOf field is one among many new keywords to fine-grain mixed type usage. Others include anyOf and allOf. These are mainly included to allow composing the type from some fixed definitions. Modularity at its best!

In general the specification v4 is all about modularity; hence it also splits the specification in different parts for the core specification, the validation schema, an hyperschema, and others. The core specification only deals with:

  • Mandate JSON reference(s) support.
  • Define canonical (and inline) addressing.
  • Define JSON in terms of equality, root schema, subschema, instance, keyword.
  • Define the schema to instance correlation.

The validation schema is concerned to ensure interoperability and to separate instance validation from children validation.

Now let's see how we can set up a JSON schema for objects that should only have a predefined set of properties following a certain naming convention.

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "properties": {
    "foo": {
      "enum": [
      	"bar",
      	"baz"
      ]
    }
  },
  "patternProperties": {
    "^[A-Za-z]+[A-Za-z0-9]*$": {}
  },
  "additionalProperties": false,
  "required": [ "foo" ]
}

The previous example does several things. For starters, we again included (defined) an explicit property called foo (this one is required), which has to be one of the given values (either the string bar or the string baz). Then we disallowed additional properties besides the ones specified, however, that still includes an infinite amount (at least in theory) of properties. Any property declared by an object that is valid in terms of the above schema has to have properties that form valid identifiers, i.e., every property name has to start with a letter and continue with a letter or number. No spaces or special symbols allowed.

Specification v6

Is the author unable to count? Well, either this or they left out v5! This is similar to other famous version skips, e.g., Windows 9, iPhone 2, Angular 3, ECMAScript 4 ... whatever the exact reasons are: v5 never landed and we've been handed over v6 directly. This version of the specification contains indeed some breaking changes to v4. This can be quite unfortunate. So let's look at the details:

There are four breaking changes in total:

  • id was replaced by $id. Reason: it is now no longer easily confused with instance properties called id.
  • Again, $id replaces id with identical behavior. Reason: the $ prefix matches the other two core keywords.
  • Specifying $ref is only allowed where a schema is expected. Reason: it is now possible to describe instance properties named $ref, which was impossible in v4.
  • exclusiveMinimum and exclusiveMaximum changed from a boolean to a number to be consistent with the principle of keyword independence.

Migration of the last two mentioned properties should go as follows: wherever one of these would be true before, change the value to the corresponding minimum or maximum value and remove the "minimum" / "maximum" keyword.

Besides these breaking changes there are some useful additions. Let's also recap them briefly:

  • Now booleans are allowed as schemas anywhere, not just at additionalProperties and additionalItems. Here, true is equivalent to {}, while false is equivalent to {"not": {}}.
  • propertyNames added.
  • contains added array keyword that passes validation if its schema validates at least one array item.
  • const added more readible form of a one-element enum.
  • required now allows an empty array indicating that no properties are required.
  • Same is true for dependencies.
  • "format": "uri-reference" added (RFC 3986 conform).
  • "format": "uri-template" added (RFC 6570 conform).
  • "format": "json-pointer" added (JSON Pointer value such as /foo/bar; do not use this for JSON Pointer URI fragments such as #/foo/bar).
  • Added examples, which is not used for validation purposes.

One of the highlights is certainly the increased richness of available data types. Now we can actually specify and distinguish between several types of URIs. Great, but it will certainly also add some confusion.

Specification v7

v7 is not as big as v6. It can be mainly seen as an update, rather than a big re-work of the earlier version. As such it is fully backwards-compatible with draft-06.

In total v7 added some new keywords. Let's enumerate them for completeness:

  • $comment was added to give some notes to schema maintainers, as opposed to description (targeted to end users).
  • if, then, else added to validation.
  • Now readOnly was moved from Hyper-Schema to validation.
  • There is also a writeOnly available for validation.
  • contentMediaType moved from Hyper-Schema to validation.
  • Finally, the same holds true for contentEncoding.

Besides all these new keywords a couple new formats have been introduced as well. We have now formats such as iri (I18N equivalent of "uri") or iri-reference. Besides some other (more exotic ones) like uri-template, idn-email, idn-hostname, or the previously mentioned json-pointer, we also finally get regex (ECMA 262 regular expressions), date (RFC 3339 full-date), as well as time (RFC 3339 full-time) back. These have been dropped after v3.

Schema Constraints

Let's look at the constraints we can set in more detail. For starters, we can set a specific type, e.g., number, or provide a set of fixed values in an enum. Both possibilities are shown below.

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "oneOf": [
    { "type": "number" },
    { "enum": [1, 2, 3, "hello"] }
  ]
}

What kind of options do we have for the different types?

Booleans

Since boolean values are so simple (just true or false) there are no other constraints.

Numbers

Numbers are tricky. We may want to impose several constraints on these. The most prominent one would be "is an integer". So instead of having some kind of magic field, we have indeed another type: integer. One option is to put a constraint on the number by only allowing values that are multiples of a given constant, e.g., the following code is a perfect match for the numeric CSS font-weight property:

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "description": "Validates the numeric value of the CSS font-weight property.",
  "type": "integer",
  "minimum": 100,
  "maximum": 900,
  "multipleOf": 100
}

By default the range boundaries (minimum and maximum) are given inclusively, i.e., we have minimum <= value <= maximum. We can make this relationship exclusive by setting the exclusiveMinimum and/or exclusiveMaximum properties accordingly.

Let's change the example to use exclusive relationships:

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "description": "Validates the numeric value of the CSS font-weight property.",
  "type": "integer",
  "minimum": 0,
  "exclusiveMinimum": true,
  "maximum": 1000,
  "exclusiveMaximum": true,
  "multipleOf": 100
}

Strings

JSON is by its nature (coming from JavaScript) quite string-heavy. So it makes sense to allow a great deal of constraints on values of this type. We start with ranges (similar to the previously discussed number ranges). Here the range is imposed on the length:

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "string",
  "minLength": 1,
  "maxLength": 3
}

This only allows strings consisting of at least a single character with a maximum of three characters.

However, while length restrictions may be handy from to time a much more precise (and powerful) restriction comes with regular expressions. In the example above nothing would prevent a user to just enter one space to fulfill our constraints. This may be wanted or not. Let's pretend the constraint was supposed to include only strings with 1 to 3 letters. How can we place this constraint in a JSON schema?

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "string",
  "pattern": "^[A-Za-z]{1,3}$"
}

The regular expressions for strings are not automatically bounded by the string. So if we want to use the expression for the whole string (and not just a subset of it), we need to use the common beginning (^) and end ($) markers within the regular expression grammar.

Arrays

One of the things we can do with arrays in JSON schema is to restrict it to unique items by setting the uniqueItems property to true. Furthermore, we can restrict the number of items in the array similar to the length of a string: here we need to use minItems and maxItems.

At this point we do not look into an example, but rather discuss a more important area regarding arrays in JSON schema. In JSON schema arrays can be verified using two different mechanisms: List validation or tuple validation. While the former assumes that the array may consist of elements from one or more types (in any order or combination), the latter defines a fixed order in which types may occur.

Let's see some list validation first:

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "array",
  "items": {
    "type": "number"
  }
}

As simple as that we have an array where any item has to be a number. Similarly, we could have specified a oneOf property to include multiple types.

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "array",
  "items": {
    "oneOf": [
      { "type": "number" },
      { "type": "string" }
    ]
  }
}

Important: The types may occur in any order and combination. Hence not only [1,2,3] is valid, but also ["1", "2", "3"], ["one", "two", 3], and [1, "two", 3]

among others.

Now let's see the difference with the tuple validation:

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "array",
  "items": [
    { "type": "number" },
    { "type": "string" }
  ]
}

We directly use an array here, instead of having an object with a oneOf operation. The difference to the list validation is that from the examples above only [1, "two", 3] would be valid in this scheme. By default, tuple validation allows having additional items (with arbitrary content). We can deactivate this behavior by setting the additionalItems property to false.

Objects

The O in JSON is standing for Object, which signals a great deal of importance for objects. Therefore, it should not be a surprise that objects in JSON schema come the most possibilities for constraints. We've already looked into properties and patternProperties. We also introduced the additionalProperties flag to disable allowing unspecified properties.

Similar to strings and arrays we can put length limitations on objects. Here, the properties to enable this constraint are called minProperties and maxProperties.

JSON
{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "type": "object",
  "minProperties": 1,
  "maxProperties": 2
}

The previously given JSON schema would only allow objects that either contain one or two (arbitrary) properties.

There are many other things we can do, but instead of just repeating the official specification we now move on to apply our knowledge in practice. We start with a popular cross-platform text editor.

Visual Studio Code

One possibility where knowledge of JSON schema is super useful is in the area of text editors. For instance, in Visual Studio Code (VSCode) JSON schema is included out-of-the-box. Why was JSON schema included in the core of VSCode? Naturally, VSCode does all its configuration via JSON files (this choice is quite natural, as VSCode is based on web technologies, where JSON is the de-facto standard anyway). Also, it deals with a whole bouquet of frameworks, libraries, and tools that also rely on JSON for their primary configuration. Providing some validation, help with descriptions, and even autocompletion with intellisense is beneficial for the user and helps to define VSCode as the editor for the modern web.

VSCode comes with a convention to customize settings on a per-project level (also per-user and global are possible). This is especially useful to make working with a project in a team much simpler. One just checks in the project settings and all other members of the same team (also using VSCode - as a prerequisite) benefit from it.

The two different scopes for settings are handled as follows:

  • User settings apply globally to any instance of VS Code opened (stored in the account directory; override global settings)
  • Project (called workspace) settings only apply when the project is opened (stored in a .vscode folder of the project root; override user settings)

The following is showing a settings.json file that is located in the .vscode folder of some project. Herein we define the following schemas to be applied.

JSON
{
  "json.schemas": [
    {
      "fileMatch": [
          "/.babelrc"
      ],
      "url": "http://json.schemastore.org/babelrc"
    },
    {
      "fileMatch": [
          "/*.foo.json"
      ],
      "url": "./myschema.json"
    }
  ]
}

While the first one matches specifically the .babelrc file (using a schema that is retrieved from the given URL), the second one is applied to all files that have the extension / suffix .foo.json. As this one seems to be rather custom we also do not have a proper schema source online, but rather take a local one from the relative path specified above.

As a result of VSCode's ability to define custom JSON validation helpers, we can enjoy intellisense (known properties will be displayed) and inline validation (errors are shown in the code editor). Thus, we can write valid JSON more efficiently than without having JSON schema.

Now let's say we are convinced that JSON schema is indeed the right choice for our own JSON files (e.g., some kind of configuration system that requires JSON files to be used).

JSON.NET Schema

In the .NET world the simplest option to support JSON schema is by referencing the JSON.NET Schema package. The standard JSON.NET package also comes with some support for JSON schema, however, this support is limited and only applicable to v3 of the specification. The library supports 100% of JSON Schema specification v3 and v4 and thus is the ideal companion to cover most use cases. It contains some simple validation helper methods and also brings in some auxiliary types to generate JSON Schemas from existing .NET types.

The following code is the minimum to get started with the library.

C#
var schema = JSchema.Parse(someJsonSchemaString);

We see that the API is actually quite similar to the standard JSON.NET library. Let's use the package to actually do some work! How about validating an existing JSON file with a schema?

C#
var obj = JObject.Parse(someJsonString);
var result = obj.IsValid(schema);

The IsValid extension method is a convenient helper to enable using the ordinary JSON.NET library with to JSON.NET Schema library. The handy helper is equivalent to the following code:

C#
var obj = JObject.Parse(someJsonString);
var result = true;

using (var reader = new JSchemaValidatingReader(obj.CreateReader()))
{
    reader.ValidationEventHandler += (s, e) => result = false;
    reader.Schema = schema;

    while (reader.Read()) ;
}

The validation event handler is quite powerful. It allows us to output the exact validation error(s) - if required. In the previous code we've decided to skip any details and just set the result to false once we encounter any error.

The API of the JSchema object also allows us to inspect the content of the schema file itself. As an example we see that a schema has optional fields such as minimumProperties, minimumItems, or minimumLength. These refer to the minimum number of properties attached to an object, the minimum number of items in an array, and the minimum number of characters in a string. In JSON.NET Schema all of these items are modeled as Int64? as they have to be optional (may be missing / unspecified) integer numbers (naturally 64-bit in JavaScript).

There are a lot more use cases to be explored, but we already have enough material to write the dedicated JSON editor that we announced earlier in this article.

The Editor

We will now write a dedicated JSON editor, which uses a JSON schema to provide a specialized user experience. This ain't no simple text editor with advanced syntax highlighting, but rather a way to create a complicated forms experience just with a JSON (schema) as basis. The output of this form is a JSON file. The editor is written in C# using the WPF framework.

We start by defining the main view models and associated views. The binding on the view models to their views is done on a app-global level. This allows us to only use view models without ever needing to create or reference views directly.

XAML
<Application x:Class="JsonEditor.App.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:vm="clr-namespace:JsonEditor.App.ViewModels"
             xmlns:v="clr-namespace:JsonEditor.App.Views"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <DataTemplate DataType="{x:Type vm:MainViewModel}">
                <v:MainView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:FileViewModel}">
                <v:FileView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:ValidationViewModel}">
                <v:ValidationView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:TypeViewModel}">
                <v:TypeView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:ArrayViewModel}">
                <v:ArrayView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:BooleanViewModel}">
                <v:BooleanView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:EnumViewModel}">
                <v:EnumView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:NumberViewModel}">
                <v:NumberView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:ObjectViewModel}">
                <v:ObjectView />
            </DataTemplate>

            <DataTemplate DataType="{x:Type vm:StringViewModel}">
                <v:StringView />
            </DataTemplate>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Now let's start with the main window. The flow is as follows:

  1. User loads a JSON file.
  2. The $schema property is retrieved.
  3. If 2. succeeds we use that JSON file as schema.
  4. Otherwise, we take a default schema.
  5. The selected schema is shown in the status bar, with the ability to change it.

Loading a JSON file is either done by drag and drop or by pressing the button in the tool bar. Drag and drop was implemented via a behavior.

C#
sealed class FileDropBehavior : Behavior<FrameworkElement>
{
    public IFileDropTarget Target
    {
        get { return AssociatedObject?.DataContext as IFileDropTarget; }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.AllowDrop = true;
        AssociatedObject.Drop += OnDrop;
        AssociatedObject.DragOver += OnDragOver;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.Drop -= OnDrop;
        AssociatedObject.DragOver -= OnDragOver;
    }

    private void OnDrop(Object sender, DragEventArgs e)
    {
        if (e.Data.GetDataPresent(DataFormats.FileDrop))
        {
            var files = e.Data.GetData(DataFormats.FileDrop) as String[];
            Target?.Dropped(files);
        }
    }

    private void OnDragOver(Object sender, DragEventArgs e)
    {
        e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) ? 
            DragDropEffects.Link : 
            DragDropEffects.None;
    }
}

The loading process looks as follows. We start with the ordinary JSON deserialization from the common JSON.NET library:

C#
private static JToken ReadJson(String path)
{
    using (var sr = new StreamReader(path))
    {
        using (var reader = new JsonTextReader(sr))
        {
            return JToken.Load(reader);
        }
    }
}

By definition the path has to be valid as the method is only used after the path has been validated (usually after an open file dialog; potentially also from the file drag and drop helper shown previously).

The idea is to use the JSON schema to determine the things to add and to provide live validation. If no schema is supplied then no validation and suggestions can be supplied. So we use the schema merely to obtain useful information instead of constraining the user too much.

In the end we have to implement a generic control that allows the user to choose the date type and can show the validation report for the given data. The generic control also hosts a custom control that handles the data for the selected type.

The following types are supported:

  • Object
  • Array
  • Number
  • Boolean
  • String
  • Enumeration

Each one tries to be user friendly in the editing mode.

The Xaml code for the Boolean input is as simple as shown below.

XAML
<UserControl x:Class="JsonEditor.App.Views.BooleanView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
    <ToggleButton IsChecked="{Binding Value}" />
</UserControl>

For an object the Xaml code is slightly larger. Essentially, we can boil it down to the following part:

 

XAML
<DockPanel LastChildFill="True">
    <Button DockPanel.Dock="Bottom"
            Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}"
            Margin="5"
            Content="Add Property" />

    <Expander IsExpanded="{Binding IsExpanded}"
              Header="Object Properties">
        <ItemsControl ItemsSource="{Binding Children}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <materialDesign:Card Margin="5">
                        <DockPanel LastChildFill="True">
                            <TextBlock DockPanel.Dock="Top"
                                       Text="{Binding Name}"
                                       FontSize="20"
                                       ToolTip="{Binding Description}"
                                       Margin="5" />

                            <StackPanel HorizontalAlignment="Right" 
                                        DockPanel.Dock="Bottom"
                                        Orientation="Horizontal" 
                                        Margin="5">
                                <Button Style="{StaticResource MaterialDesignToolButton}"
                                        Command="{Binding DataContext.RemoveProperty, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ItemsControl}}}"
                                        CommandParameter="{Binding}"
                                        Padding="2 0 2 0"
                                        materialDesign:RippleAssist.IsCentered="True"
                                        Width="30"
                                        ToolTip="Remove Property">
                                    <materialDesign:PackIcon Kind="Delete" />
                                </Button>
                            </StackPanel>

                            <ContentControl Content="{Binding}" />
                        </DockPanel>
                    </materialDesign:Card>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Expander>
</DockPanel>

Adding a new property requires to open a special dialog first, which is implemented via the DialogHost control. The name of the new property is then selected from a combobox, which shows all the known options and allows entering arbitrary text. The text is validated with help from the JSON schema file (if there are any patternProperties defined).

The code for the container (generic control) is also not really black magic.

XAML
<DockPanel LastChildFill="True">
    <ContentControl Content="{Binding Validation}"
                    DockPanel.Dock="Top" />

    <ContentControl Content="{Binding Type}"
                    DockPanel.Dock="Top" />
    
    <ContentControl Content="{Binding Value}" />
</DockPanel>

All in all every item consists of three parts: its validation summary, the type (selector) of the item, and its current value. Every control is selected from the binding engine.

Using the code

The attached code can be easily customized to only support a specific schema, or even better enhance some special schema(s) with logical constraints or selections in the UI, which would be impossible from a pure schema file.

The application is a small WPF app, which gives you the ability to graphically create or edit a JSON file. By default, the JSON file is schemaless, accepting any kind of input without any help. If a "$schema" annotation is found the given schema is selected. Otherwise, a new schema can always be manually set.

The schema determines what options to display and how the validation goes.

(tbd screenshots and usage)

Points of Interest

In this article we have been introduced to the JSON schema specification, what it is and how we can leverage it to provide a better user experience.

We have seen that editors profit a lot from having defined schemas. Also we were able to build a simple JSON schema editor in WPF with just a few lines of code.

Personally, I've witnessed countless occassions where JSON schema made the solution better or work at all. For instance, in an application I had a large configuration system. However, the problem with such a system is never that the configuration is getting too large, but rather that the validation complexity is growing exponentially. Specifying in code how the validation should be done is tedious and error-prone. Much better is a JSON schema, which is validated automatically and comes with correct error messages that can be displayed to the end-user.

Another example was an API definition for some JSON input files. In the project where I've encountered this the whole specification was written in a Microsoft Word document. Not really pleasant, right? Developers had to look at the Word document, compose the JSON, and finally try the JSON against the production system. This all effort just to find out that the documentation was wrong (or outdated) at one particular area... Again not pleasant. A JSON schema could have replaced the Word document efficiently. Furthermore, it could not only be auto-generated from the source, it could also be consumed by text editors or be transpiled to Markdown or some PDF for a print reference. And the best part: Validation can happen automatically! No more manual upload and testing against the production code.

Where do you see JSON schema shine?

References

This is a list of references that I found useful or relate to information being displayed in this article.

History

  • v1.0.0 | Initial Writeup | 22.01.2017
  • v1.0.1 | Initial Release | 09.03.2018
  • v1.0.2 | Added Sources | 13.03.2018
  • v1.1.0 | Information on v6 and v7 | 30.04.2018

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)