Contents
- Introduction
- Background
- JSON Schema
- Specification v3
- Specification v4
- Specification v6
- Specification v7
- Schema Constraints
- Visual Studio Code
- JSON.NET Schema
- The Editor
- Using the code
- Points of Interest
- References
- 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.
{
"$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:
{
"$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.
{
"$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:
{ }
{ "foo": "bar" }
{ "bar": 3 }
{ "baz": false }
{ "foo": "bar", "bar": 4.1, "baz": [] }
However, the upcoming JSON snippets are all invalid:
[]
{ "foo": false }
{ "bar": "foo" }
{ "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.
{
"$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:
{
"$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.
{
"$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:
{
"$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.
{
"$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:
{
"$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.
{
"$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.
{
"$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:
{
"$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:
{
"$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:
{
"$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?
{
"$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:
{
"$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.
{
"$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:
{
"$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
.
{
"$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.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.
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?
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:
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.
<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:
- User loads a JSON file.
- The
$schema
property is retrieved. - If 2. succeeds we use that JSON file as schema.
- Otherwise, we take a default schema.
- 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.
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:
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.
<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:
<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.
<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