Typed JSON is a format for defining structured JSON data, that can be used by type systems or contract / guard librarires to allow cross-language type-safety guarantees.
Typed JSON enables type information to be preserved between typed and untyped languages. It can be used by developer tools and compilers to decrease the burden of validating and using data.
Typed JSON supports:
- Primitive types: int, float, etc.
- Basic data structures: collections, records, and tuples
- Constants: values such as
4andtrue - Union types: which can express Algebraic Data Types and sub-types
Every type is associated with a unique URI. This can be used simply as a unique identifier, or it can be associated with a type definition.
Typed JSON defines base primitive types that are common to almost all languages. Each primitive type is associated with specific URIs.
{
"bool" : "http://typed-json.org/#boolean",
"int" : "http://typed-json.org/#int",
"float" : "http://typed-json.org/#float",
"string": "http://typed-json.org/#string",
"null" : "http://typed-json.org/#null"
}Primitive types can be referenced by their URI or aliased as a local type. In the example above, we created a local type alias for all of our primitive types. These aliases can be used later to make more complex types easier to read.
The null primitive type represents the absense of value. It is actually
a type of its own, not a possible value for all objects.
Since null is valid JSON primitive it can be used instead of URI
http://typed-json.org/#null. For example type empty can be defined
as an alias to http://typed-json.org/#null as follows: { "empty": null }
Typed JSON is great for defining composite types. The key composite types are:
- Records: a group of named fields, each associated with a type (similar to objects)
- Collections: arrays or lists of a single type
- Tuples: fixed sized containers with mixed types
Record types represent JSON objects with a specific structure. They are defined in terms of field type signatures:
{
"point": {
"x": "http://typed-json.org/#int",
"y": "http://typed-json.org/#int"
}
}Above JSON defines point type that must have x and y fields
of int type. This example uses full URIs to for field type definitions,
but that's redundant and could be expressed in more eloquent manner:
{
"int": "http://typed-json.org/#int",
"point": { "x": "int", "y": "int" }
}Composite data type definitions can refer to other composite types:
{
"int": "http://typed-json.org/#int",
"point": { "x": "int", "y": "int" },
"line": {
"start": "point",
"end": "point"
}
}Collections, like arrays (or lists, depending on the language), must contain items of specified type and are defined as follows:
{
"int": "http://typed-json.org/#int",
"point": { "x": "int", "y": "int" },
"shape": ["point"]
}Note: The example above defines a shape type that is a
collection of an arbitrary number of point items
The following JSON data would conform to the shape type:
[]
[{"x":0, "y":0}]
[{"x":0, "y":0}, {"x": 0, "y": 10}]
[{"x":0, "y":0}, {"x": 0, "y": 10}, {"x": 10: "y": 10}]It is also possible to define fixed-size collections:
{
"int": "http://typed-json.org/#int",
"point": ["int", 2],
"line": ["point", 2]
}The following JSON data would conform to the line type:
[[0,0], [0,10]]
[[0,0], [10,10]]Tuples are fixed size containers with mixed types. In contrast to regular fixed size arrays, they define element types by index.
{
"int": "http://typed-json.org/#int",
"string": "http://typed-json.org/#string",
"color": "string",
"point": { "x": "int", "y": "int" },
"pixel": {
"0": "point",
"1": "color"
}
}The above pixel type defines a structure for values like:
[{x:0, y:0}, "red"]
[{x:0, y:12}, "green"]Note: "color" is just an alias for a string with a different semantic meaning. It's useful to give semantic meaning to entities used in type definitions, as it allows changing the types of those entities independently from computed types. This makes it easy to replace color with a record of RGB values at some point.
New primitives can be defined by aliasing existing primitive types
and adding some additional metadata. For example type digit can
be defined as:
{
"digit": "http://typed-json.org/#int",
"digit:meta": {
"min": 0,
"max": 9
}
}Note that above definition uses "digit:meta" key to define metadata
for the digit type. Metadata keys must be mapped to objects whose
fields are not specified by this format. Different environments may
choose to support metadata fields. For example, the digit type's metadata
specifies range of ints, but if runtime does not support ranges, it
will still treat digit as int type.
Specification recognizes constants of string, integer, float
and boolean types:
{
"readyStatus": 1,
"readyState": "'complete'",
"yes": true,
}Above data structure defines type readyStatus constant of int
type that will only match 1. Type yes is a boolean that is
true. Type readyState is a constant primitive that matches
'complete' string in JSON although in some languages that could
translate to more appropriate contant values like keywords
in clojure.
In languages like Elm and Haskell, the name of the constant type could be used to create a simple Algebraic Data Type:
data ReadyStatus = ReadyStatus
data ReadyState = ReadyState
data Yes = YesComposite types can also be defined in form of [union types][] to allow structures that can contain non-homogeneous types:
{
"string": "http://typed-json.org/#string",
"pending": true,
"complete": { "data": "string" },
"status": "pending|complete"
}This is very natural in untyped languages like JavaScript, but it also maps nicely onto [Algebraic Data Types][] (ADTs) in functional languages like Elm and Haskell. It also maps onto subclasses in OO languages like Java.
In Haskell, the "status" type would be represented as:
data Status = Pending | Complete { data :: String }As we saw above, unions can be defined over constant types:
{
"yes": "'yes'",
"no": "'no'",
"show": "yes|no"
}In JavaScript, the values passed along would be a string 'yes' or
'no'. A statically-typed functional language like Elm or Haskell
would represent this as:
data Show = Yes | NoIt is guaranteed that members of a union type are named, so it is always safe to map onto an ADT or class heirarchy.
There is also syntax sugar to express above in more concise way:
{
"show": "'yes'|'no'"
}It is still possible to map this onto ADTs because strings implicitly have names. Unlike strings, constant integers and floats must be defined explicitly to ensure they have a name.
{
"two": 2,
"three": 3,
"five": 5,
"seven": 7,
"prime-digits": "two|three|five|seven"
}In JavaScript, this would just send an integer over the wire. In Haskell or Elm, this would be represented as:
data PrimeDigits = Two | Three | Five | SevenThis lets you work with the colloquial representation in very different languages.