JSON Schema (Pre-v10)
This documentation only applies to Manatee.Json versions 9 and earlier. To work with JSON Schema in Manatee.Json version 10, please refer to the latest JSON Schema page.
The occasion may arise when you wish to validate that a JSON object is in the correct form (has the appropriate keys and the right types of values). Enter JSON Schema. Much like XML Schema with XML, JSON Schema defines a pattern for JSON data. A JSON Schema validator can verify that a given JSON object meets the requirements as defined by the JSON Schema. This validation can come in handy as a precursor step before deserializing.
More information about JSON Schema can be found at json-schema.org.
Building JSON Schema
In Manatee.Json, JSON Schema are defined using the JsonSchema
class, which can be found in the Manatee.Json.Schema
namespace. This object contains the properties you'll need to validate any JSON object. Let's take a look at this object in a bit more detail to understand all of the parts of a JSON Schema.
Property | Validates | Supported by draft | Description |
---|---|---|---|
Id |
Any | 4/6/7 | This is a string value which defines an id for this schema. Value must be a URI. When deserialized, a schema will automatically register itself with JsonSchemaRegistry. For draft 4, this is represented in JSON as the `id` property; for draft 6, this has been updated to `$id`. |
Schema |
Any | 4/6/7 | This will usually be the same value as the Id property, but may be different. Value must be a URI. It is used to identify a schema which governs the valid shape of this schema. If left null, the default to be used is specified in the `JsonSchemaFactory` static class. |
Title |
Any | 4/6/7 | Provides a title for this schema. This is informational only and does not affect validation. |
Description |
Any | 4/6/7 | Provides a description for this schema. This is informational only and does not affect validation. |
Default |
Any | 4/6/7 | Defines a default value. This may be any value. |
MultipleOf |
Numbers | 4/6/7 | Defines a required divisor for accepted values. |
Maximum |
Numbers | 4/6/7 | Defines a maximum for accepted values. |
ExclusiveMaximum |
Numbers | 4/6/7 | draft 4 (boolean): Defines whether the value in the Maximum property is acceptable.
draft 6/7 (number): Defines an exclusive maximum. |
Minimum |
Numbers | 4/6/7 | Defines a minimum for accepted values. |
ExclusiveMinimum |
Numbers | 4/6/7 | draft 4 (boolean): Defines whether the value in the Minimum property is acceptable.
draft 6/7 (number): Defines an exclusive minimum. |
MaxLength |
Strings | 4/6/7 | Defines a maximum number of characters. |
MinLength |
Strings | 4/6/7 | Defines a minimum number of characters. |
Pattern |
Strings | 4/6/7 | Defines a regular expression for which acceptable values must pass validation. |
AdditionalItems |
Arrays | 4/6/7 | Defines whether additional items are allowed in acceptable values. May be True , False , or a JSON schema. |
Items |
Arrays | 4/6/7 | Defines a JSON schema or sequence of JSON schemata to validate contained elements. |
MaxItems |
Arrays | 4/6/7 | Defines a maximum number of elements allowed. |
MinItems |
Arrays | 4/6/7 | Defines a minimum number of elements allowed. |
UniqueItems |
Arrays | 4/6/7 | Defines whether each element must be unique. |
MaxProperties |
Objects | 4/6/7 | Defines a maximum number of properties allowed. |
MinProperties |
Objects | 4/6/7 | Defines a minimum number of properties allowed. |
AdditionalProperties |
Objects | 4/6/7 | Defines whether any keys not listed in the Properties property may appear as well as the schema to validate any such properties. See AdditionalProperties and PatternProperties below for more information. |
Properties |
Objects | 4/6/7 | Defines a collection of keys which may appear in the object being validated as well as the schema to validate the value under that key. |
Definitions |
Any | 4/6/7 | Defines a collection of type definitions that may be referenced throughout this schema. |
PatternProperties |
Objects | 4/6/7 | Defines criteria which any additional keys must adhere as well as the schema to validate those properties. See AdditionalProperties and PatternProperties below for more information. |
Dependencies |
Objects | 4/6/7 | Defines any keys on which other keys depend. For instance, the JSON Schema draft 4 document defines that the exclusiveMaximum property requires that the maximum property be present in the object being validated. |
Enum |
Any | 4/6/7 | Defines a discrete collection of acceptable values. |
Type |
Any | 4/6/7 | Defines a type or a collection of simple JSON Schema types acceptable. |
AllOf |
Any | 4/6/7 | Defines a collection of JSON Schema, all of which acceptable values must pass. |
AnyOf |
Any | 4/6/7 | Defines a collection of JSON Schema, one or more of which acceptable values must pass. |
OneOf |
Any | 4/6/7 | Defines a collection of JSON Schema, exactly one of which acceptable values must pass. |
Not |
Any | 4/6/7 | Defines a JSON Schema for which acceptable values must fail. |
Format |
Strings | 4/6/7 | Allows special validation on strings. |
Required |
Objects | 4/6/7 | Lists names of required properties. |
ExtraneousDetails |
Any | 4/6/7 | Stores any properties not explicitly defined in the JSON Schema specifications to be added to the schema. There is no validation on these properties. |
PropertyNames |
Any | 6/7 | Defines a schema for any property names that aren't explicitly defined. Differs from AdditionalProperties in that this looks at the property name instead of the property value. |
If/Then/Else |
Any | 7 | Each property defines a schema. If the `If` schema passes validation, the `Then` schema is validated, otherwise the `Else` schema is validated. |
Type Definitions
Type definitions are typically used to extend the basic definitions: array, boolean, integer, null, number, object, and string. Each of these schema types have their own distinct properties. For instance, the string schema type can have a minimum length requirement. Type definitions provide a method for naming a string type with a minimum length so that it can be referenced anywhere within the JSON Schema document.
To reference a type definition from within the JSON Schema document, the relative path to the definition must be given using a slash-delimited property string with a pound sign specifying the root of the document. Typically a reference will appear in the following form:
#/definitions/[type name]
Ideally all type definitions will be contained in the root schema object's Definitions
collection, however they could technically appear in non-schema-standard properties (These will appear in the ExtraneousDetails
collection on the JsonSchema
object.). References to type definitions can even be made from other type definitions.
See the References section below for more information on references.
Properties
The most important part of any JSON Schema is the collection of properties it defines. This is what allows the schema to validate that a JSON object contains the right kind of data.
Let's start with an example. Suppose we have a schema which defines a single property:
{"properties":{"myProperty":{}}}
which can declared in Manatee.Json by
var schema = new JsonSchema07
{
Properties = new Dictionary<string, IJsonSchema>
{
["myProperty"] = new JsonSchema07()
}
}
This schema is not very interesting (or useful) as it only defines that the property should exist (although it's not required), and it doesn't define a type for it. Suppose we pass the following JSON objects through the validator:
{}
{"myProperty":false}
{"myProperty":"some string"}
{"otherProperty":35.4}
The schema will validate all of them as compliant because it has no type restrictions for the property. Let's specify it as string.
{"properties":{"myProperty":{"type":"string"}}}
which is declared in Manatee.Json by
var schema = new JsonSchema07
{
Properties = new Dictionary<string, IJsonSchema>
{
["myProperty"] = new JsonSchema07 {Type = JsonSchemaType.String}
}
}
Now, the JSON object with a value of false
will pass validation; the others will pass. (The ones missing the property pass because it's not listed as required.)
The JsonSchemaType
enumeration is marked with a Flags
attribute, so that they can be combined. For instance, the JSON Schema draft 6 metaschema declares a type of both boolean
and object
. This is done in an array
{"type": ["object", "boolean"]}
This is declared in code using the |
operator.
var schema = new JsonSchema07 { Type = JsonSchemaType.Object | JsonSchemaType.Boolean };
AdditionalProperties
and PatternProperties
According to the JSON Schema documentation, these two properties have a somewhat unique interaction. In short, the rules governing the validation of these properties are as follows:
PatternProperties
defines a collection of Regular Expression/JSON Schema pairs. If a property name in an object matches a Regular Expression, it must also pass the defined JSON Schema.AdditionalProperties
may be true, false, or a valid schema.- For
JsonSchema04
, this is implemented via two static fields on theAdditionalProperties
type (True
andFalse
) and a general constructor which allows the injection of anIJsonSchema
implementation. - For
JsonSchema06
andJsonSchema07
, the property is merelyIJsonSchema
since drafts 6 and 7 support booleans as valid schemas.
- For
Now here's the tricky part. The behavior of these properties depends on the value of AdditionalProperties
.
- If
AdditionalProperties
is true- The properties listed in
Properties
are evaluated per normal. PatternProperties
applies to all properties, even those inProperties
.
- The properties listed in
- If
AdditionalProperties
is a valid schema- The properties listed in
Properties
are evaluated per normal. PatternProperties
applies to all properties, even those inProperties
.- The
AdditionalProperties
schema is used to validate all properties not handled byProperties
orPatternProperties
.
- The properties listed in
- If
AdditionalProperties
is false- The properties listed in
Properties
are evaluated per normal. PatternProperties
applies only to properties not defined inProperties
.- No other properties are allowed.
- The properties listed in
The default value for AdditionalProperties
is the empty schema, which validates all additional properties (like True
).
References
Suppose we want to impose a minimum string length of 10 on the myProperty
value from the above schema. We can define the schema in two ways:
By explicitly defining the type in the property definition
{
"properties":{
"myProperty":{
"type":"string",
"minLength":10
}
}
}
or by defining the type and creating a reference to it:
{
"definitions":{
"stringWithMinLength10":{
"type":"string",
"minLength":10
}
},
"properties":{
"myProperty":{"$ref":"#/definitions/stringWithMinLength10"}
}
}
As you can see, the reference is defined by an object with a special key: "$ref"
. The value is the path to the definition relative to the root of the schema. The value may also be a URI where the schema can be downloaded, such as http://json-schema.org/geo
. Now that you understand how type definitions and references work, let's take a look at the above schema declared with Manatee.Json.
var schema = new JsonSchema07
{
Definitions = new Dictionary<string, object>
{
["stringWithMinLenth10"] = new JsonSchema06
{
Type = JsonSchemaTypeDefinition.String},
MinLength = 10
}
},
Properties = new Dictionary<string, object>
{
["myProperty"] = new JsonSchemaReference(
"#/definitions/stringWithMinLength10",
typeof(JsonSchema06))
}
}
NOTE We need to tell the JsonSchemaReference
class what version of schema we'd like constructed if the schema doesn't itself declare a version.
The declaration is very similar to the schema itself. Defining a new type is typically done when that type is used for several properties. In the above example, it is somewhat unnecessary.
External references are a useful way to organize complex type hierarchies; they allow a type defined in one schema to be referenced in another schema. For example, the following are all valid syntax:
{"$ref": "http://example.com/otherSchema.json"}
{"$ref": "http://example.com/otherSchema.json#/definitions/exampleType"}
{"$ref": "./otherSchema.json"}
{"$ref": "../otherSchema.json#/definitions/exampleType"}
In other words, references can use either absolute or relative URIs to indicate external schemas, in addition to an optional JSON pointer to the part of the file being referenced. External references do not need to comply to the same meta-schema as the file doing the referencing; a schema complying to draft 4 can reference an external schema complying to draft 7, and vice versa. Manatee.Json can automatically access external references using the JsonSchemaFactory.FromJson function, which has an optional parameter for the document's URI — necessary information for resolving relative URIs.
As a final note on properties, let's take a look at how the latest version of JSON Schema (draft 4) defines properties which are required by making the myProperty
property required. The schema itself would look like this:
{
"properties":{
"myProperty":{
"type":"string",
"minLength":10
}
},
"required":["myProperty"]
}
The schema object has a property which declares which properties are required. Manatee.Json is very similar:
var schema = new JsonSchema06
{
Properties = new Dictionary<string, IJsonSchema>
{
["myProperty")] = new JsonSchema06
{
Type = JsonSchemaTypeDefinition.String,
MinLength = 10
}
},
Required = new [] {"myProperty"}
};
One Schema to Rule Them All
It may be appropriate for a schema to accept multiple kinds of data. For instance, if you're reading from an API, and your DTOs (Data Transfer Objects) can sometimes be represented as the full object and sometimes by only a string (like an ID), it's useful to have a schema for that object that can validate both cases.
Let's consider the following DTO:
public class SampleDto
{
public string Id { get; set; }
public double Score { get; set; }
public string Username { get; set; }
}
A schema could be defined that could accept the complete object
{
"Id" : "b5e72f0a-cf1b-4732-bb83-3bb756c77ee1",
"Score" : 9.5,
"Username" : "gregsdennis"
}
or simply a string (interpreted to be the ID)
"b5e72f0a-cf1b-4732-bb83-3bb756c77ee1"
Such a schema could look like
{
"type" : [ "object", "string" ],
"properties" : {
"Id" : {
"type" : "string",
"pattern" : "^[a-fA-F0-9]{8}-([a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12}$"
},
"Score" : {
"type" : "number",
"minimum" : 0
}
"Username" : { "type" : "string" }
},
"pattern" : "^[a-fA-F0-9]{8}-([a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12}$"
}
This schema defines:
- that the JSON could be a string or an object
- that, if an object, it should have properties
Id
as a string matching a GUID RegExScore
as a number greater than or equal to 0Username
as any string
- that, if a string, it should match a GUID RegEx
This would be built in Manatee.Json as
var guidPattern = "^[a-fA-F0-9]{8}-([a-fA-F0-9]{4}-){3}[a-fA-F0-9]{12}$";
var schema = new JsonSchema
{
Type = JsonSchemaType.String | JsonSchemaType.Object,
Properties = new Dictionary<string, object>
{
["Id"] = new JsonSchema06
{
Type = JsonSchemaType.String,
Pattern = guidPattern
},
["Score"] = new JsonSchema06
{
Type = JsonSchemaType.Number,
Minimum = 0
},
["Username"] = new JsonSchema06
{
Type = JsonSchemaType.String
},
},
Pattern = guidPattern
}
The key here to remember is that Properties
only validates when the JSON is an object, and Pattern
only validates when the JSON is a string. Otherwise, these are ignored. The Type
property ensures that it fails for anything except strings or objects.
That covers all of the basics for defining and building schema directly. As expected, schema can also be downloaded and deserialized. This will be covered in the Schema Serialization section below.
NOTE On occasion, an object may have a property which is of the same type. In order to declare this kind of property, you should use the static property Root
defined on the appropriate schema class. This will instruct the reference to validate the property against the root schema.
Validation
The purpose of JSON Schema is to validate that a JSON value meets certain criteria. Let's take a look at how Manatee.Json schema objects provide this service.
Every schema object is required to implement the IJsonSchema
interface. This interface defines a single method:
SchemaValidationResults Validate(JsonValue json, JsonValue root = null)
It is this method which we will use to validate incoming JSON values. Let's begin with our aforementioned schema and JSON objects:
{
"properties":{
"myProperty":{
"type":"string",
"minLength":10
}
},
"required":["myProperty"]
}
{}
{"myProperty":false}
{"myProperty":"some string"}
{"otherProperty":35.4}
"nonObject"
To validate these, all we have to do is pass these into our schema's Validate()
method.
var schema = GetSchema(); // defines the schema we have listed above.
var emptyJson = new JsonObject();
var booleanJson = new JsonObject { {"myProperty", false} };
var stringJson = new JsonObject { {"myProperty", "some string"} };
var numberJson = new JsonObject { {"otherProperty", 35.4} };
var nonObject = (JsonValue)"nonObject";
var emptyResults = schema.Validate(emptyJson);
var booleanResults = schema.Validate(booleanJson);
var stringResults = schema.Validate(stringJson);
var numberResults = schema.Validate(numberJson);
var nonObjectResults = schame.Validate(nonObject);
IMPORTANT You may have noticed that the second parameter is not used. This parameter is used internally for validating subschema and resolving references and should not be used explicitly.
The various results objects are of type SchemaValidationResults
, which has two properties:
- IsValid - Indicates if the tested JSON value is valid,
- Errors - A collection of errors found while validating. Each error lists a JSONPath to the problem element and a message which describes the issue.
In the above example, the following would be reported:
emptyJson
andnumberJson
failed because"myProperty"
was not found.booleanJson
failed because"myProperty"
is of the wrong type.stringJson
passed validation.nonObject
also passed validation becauseproperties
andrequired
ignore non-object JSON.
Schema Serialization
Of what use would JSON Schema objects be to a developer if they were not serializable to and from JSON? Not a whole lot. Therefore, Manatee.Json's schema objects are designed to be completely serializable to and from JsonObject
s (and subsequently to and from strings).
var schema = GetSchema();
var json = schema.ToJson(null);
This can also be done through a JsonSerializer
instance, of course. The serializer will yield the same results.
NOTE The null
passed into the ToJson()
method indicates that a serializer instance is not required to serialize the schema classes.
For deserialization, you will need to go through the JsonSchemaFactory.FromJson()
static method. Simply pass your JSON in, and you get the appropriate JSON schema out.
var json = GetSchemaJson();
var schema = JsonSchemaFactory.FromJson(json);
You may prefer to use deserialization combined with JSON object construction to generate your schema.
var json = new JsonObject
{
{"properties", new JsonObject
{
{"myProperty", new JsonObject
{
{"type", "string"},
{"minLength", 10}
}
}
}
}
{"required", new JsonArray {"myProperty"}}
};
var serializer = new JsonSerializer();
var schema = serializer.Deserialize<IJsonSchema>(json);
As you can see, the schema definition is easy to read. The downside to this approach is that you don't get the compile-time checking of the JsonSchema0x
classes.
This is also useful when your schema is defined at a URI. Simply download the content, parse it, and run it through the schema factory.
var json = JsonValue.Parse(new WebClient().DownloadString("http://json-schema.org/geo"));
var schema = JsonSchemaFactory.FromJson(json);
Schema Version Selection
JsonSchemaFactory
will make a best guess when deserializing schemata, but many times, both drafts may work for a given schema. In these cases a default implementation must be used.
To determine the schema version, use JsonSchemaFactory.SetDefaultSchemaVersion<T>()
and pass in either JsonSchema04
, JsonSchema06
, or JsonSchema07
. This will instruct the system to create instances of this type when deserializing and dereferencing schemata.
NOTE This method will throw an exception if any type outside of these two are used.
Somewhat related to schema serialization is the ability to download schema directly. When you need to do this, it is suggested that you use the JsonSchemaRegistry.Get()
static method. Just pass in a URI, and it will download and deserialize the schema automatically. The benefit to using this method is that the class will also cache the schema for future use. If there are any references to that URI, it will simply return this schema rather than downloading and deserializing again.
This static class also exposes additional methods which allow you to manage the cache of schemata.
Register(JsonSchema)
- Adds the schema to the cache.Unregister(JsonSchema)
- Removes the schema from the cache.Unregister(string)
- Removes a schema registered to the specified URI from the cache.
NOTE To register a schema, it must have a valid id
.
Final Notes
Manatee.Json's schema implementation has been designed using the latest two drafts of JSON Schema: draft 4 and draft 6. Included with these schemas are self-validating schema (metaschema) which serve as a basis for all other schema. This means that any schema you create must pass validation by this base schema. Furthermore, both schema classes define their metaschema and the empty schema as static properties.
As drafts 6 and 7 support booleans as valid schema, implicit casts have been created from bool
to JsonSchema06
and JsonSchema07
. These are translated to the static properties True
and False
on their respective types. These casts will not work implicitly when assigning to a property of type IJsonSchema
; an explicit cast will be required.