JsonSchemaProvider 0.1.0

Edit this page

Scope

The JSON Schema type provider only supports JSON schemas that have an object type at the root level. That is, the provider is able to deduce a type from the the following schema:

{
  "type": "object",
  "properties": {
    "X": {
      "type": "string"
    },
    "Y": {
      "type": "string"
    }
  }
}

In contrast, it will, e.g., fail on this schema at compile-time:

{
  "type": "array",
  "items": {
    "type": "int"
  }
}

The rationale is that a JSON array should be mapped to an F# array. This would entail that the provided type is only a synonym for an array type and not a distinguished provided type.

The following table show the supported JSON schema types and the F# types they are mapped to:

JSON Schema type

F# type

string

string

boolean

bool

integer

int

number

float

array

list

object

class

Usage

Add the JsonSchemaProvider NuGet package to your project:

dotnet add package JsonSchemaProvider

A JSON schema can either be given as a literal string or by a file path.

Open the JsonSchemaProvider namespace and pass the literal string to the JsonSchemaProvider via the schema argument to provide a type from that schema (Xyz in this case):

open JsonSchemaProvider

[<Literal>]
let schema =
    """
    {
      "type": "object",
      "properties": {
        "X": {
          "type": "string"
        },
        "Y": {
          "type": "string"
        },
        "Z": {
          "type": "integer"
        }
      }
    }"""

type Xyz = JsonSchemaProvider<schema=schema>

To read the JSON schema from a file, the static parameter schemaFile can be used:

type FromFile = JsonSchemaProvider<schemaFile=PathToSchemaFile>

Creating values of the provided type

There are two ways to create values of the provided type:

You can use the static Create method that takes keyword arguments for the properties of the root-level object:

let xz1 = Xyz.Create(X = "x", Z = 1)

Alternatively, you can use the static Parse method to parse a JSON into a value of the Xyz type and, at the same time, validating it:

let xz2 = Xyz.Parse("""{"X": "x", "Z": 1}""")

For instance, the following invalid JSON value will be rejected with a message indicating the expected string for the X property.

try
    Xyz.Parse("""{"X": 1, "Z": 1}""") |> ignore
with :? System.ArgumentException as ex ->
    printfn "Error: %s" ex.Message

Note: This particular error would have been prevented using the Create method since assigning an int to the X property is a static type error. When parsing, this error is delayed to runtime, of course.

Similarly, other validation errors like ranges or regular expressions are postponed to runtime.

Here is an example:

[<Literal>]
let rangeSchema =
    """
    {
      "type": "object",
      "properties": {
        "from": {
          "type": "integer",
          "minimum": 0
        },
        "to": {
          "type": "integer",
          "maximum": 10
        }
      }
    }"""

type Range = JsonSchemaProvider<schema=rangeSchema>

try
    Range.Create(from = 0, ``to`` = 11) |> ignore
with :? System.ArgumentException as ex ->
    printfn "Error: %s" ex.Message

In the examples so for, the properties were all optional. Required properties cannot be omitted in the arguments to Create without leading to a compile time error.

[<Literal>]
let rangeRequiredSchema =
    """
    {
      "type": "object",
      "properties": {
        "from": {
          "type": "integer",
          "minimum": 0
        },
        "to": {
          "type": "integer",
          "maximum": 10
        }
      },
      "required": ["from", "to"]
    }"""

type RangeRequired = JsonSchemaProvider<schema=rangeRequiredSchema>

The call

let range = RangeRequired.Create(from=1)

leads to the error

The member or object constructor 'Create' requires 1 argument(s). The required signature
is 'JsonSchemaProvider<...>.Create(from: int, ``to`` : int) : JsonSchemaProvider<...>'.

Selecting from values of the provided type

The properties of a JSON object are exposed as F# properties of the JSON value. An optional JSON property of type T is exposed as F# property of type T' option where T' is the F# type the JSON type T is mapped to.

Likewise, required properties are mapped directly from T to T'.

Consider the following schema to store names with middle initials as they are common in the US:

[<Literal>]
let nameSchema =
    """
    {
      "type": "object",
      "properties": {
        "firstName": {"type": "string"},
        "middleInitials": {"type": "string"},
        "lastName": {"type": "string"}
      },
      "required": ["firstName", "lastName"]
    }"""

type Name = JsonSchemaProvider<schema=nameSchema>

For the following name

let name1 =
    Name.Create(firstName = "Donald", middleInitials = "EK", lastName = "Knuth")

we have a string for the first name

name1.firstName
input.fsx (1,1)-(1,6) typecheck error The value, namespace, type or module 'name1' is not defined. Maybe you want one of the following:
   nameof

and a string option for the middle initials:

name1.middleInitials
input.fsx (1,1)-(1,6) typecheck error The value, namespace, type or module 'name1' is not defined. Maybe you want one of the following:
   nameof

Nested objects

It is common that JSON schemas specify nested objects. Consider, e.g., the following JSON Schema to store the global position of a city:

[<Literal>]
let cityPosition =
    """
    {
      "type": "object",
      "properties": {
        "city": {"type": "string"},
        "globalPosition": {
          "type": "object",
          "properties": {
            "lat": {"type": "number"},
            "lon": {"type": "number"}
          },
          "required": ["lat", "lon"]
        }
      },
      "required": ["city", "globalPosition"]
    }"""

type CityPosition = JsonSchemaProvider<schema=cityPosition>

The JSON schema provider creates inner types for nested objects that have the name pObj where p is the name of the property with the nested object type:

let position =
    CityPosition.globalPositionObj.Create(lat = 52.520007, lon = 13.404954)

let berlinPosition = CityPosition.Create("Berlin", position)
input.fsx (2,5)-(2,17) typecheck error The value, namespace, type or module 'CityPosition' is not defined.
input.fsx (4,22)-(4,34) typecheck error The value, namespace, type or module 'CityPosition' is not defined. Maybe you want one of the following:
   position

Nested properties can be selected in the expected way:

let berlinLat = berlinPosition.globalPosition.lat
input.fsx (1,17)-(1,31) typecheck error The value, namespace, type or module 'berlinPosition' is not defined.

Arrays

JSON arrays are mapped to F# lists:

[<Literal>]
let temperatures =
    """
    {
      "type": "object",
      "properties": {
        "location": {"type": "string"},
        "values": {
          "type": "array",
          "items": {"type": "number"}
          }
      },
      "required": ["location", "values"]
    }"""

type Temperatures = JsonSchemaProvider<schema=temperatures>

let temps = Temperatures.Create("Munich", [ 11.0; 12.0; 11.6; 12.1 ])

This enables the usual list functionality like, e.g., slicing:

for temp in temps.values[1..3] do
    printfn "%f" temp

Arrays can be nested:

[<Literal>]
let table =
    """
    {
      "type": "object",
      "properties": {
        "cells": {
          "type": "array",
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "align": {"type": "string"},
                "content": {"type": "string"}
              }
            }
          }
        }
      },
      "required": ["cells"]
    }"""

type Table = JsonSchemaProvider<schema=table>

let beginEndTable =
    Table.Parse(
        """
        {
          "cells": [
            [
              {"align": "right", "content": "Begin"},
              {"align": "left", "content": "10:13:00"}
            ],
            [
              {"align": "right", "content": "End"},
              {"align": "left", "content": "12:15:00"}
            ]
          ]
        }"""
    )

The nested arrays in nestedArrayExample can be flattened like this, e.g.:

beginEndTable.cells |> List.concat |> List.map (fun props -> props.ToString())
input.fsx (1,1)-(1,14) typecheck error The value, namespace, type or module 'beginEndTable' is not defined.

This example also shows that the class name of the objects nested inside the arrays is derived from the property name that hosts the arrays. In this case it is cellsObj:

Table.cellsObj.Create(align = "left", content = "Stop 1")
Multiple items
namespace JsonSchemaProvider

--------------------
type JsonSchemaProvider = inherit NullableJsonValue
Multiple items
type LiteralAttribute = inherit Attribute new: unit -> LiteralAttribute

--------------------
new: unit -> LiteralAttribute
[<Literal>] val schema: string = " { "type": "object", "properties": { "X": { "type": "string" }, "Y": { "type": "string" }, "Z": { "type": "integer" } } }"
type Xyz = JsonSchemaProvider<...>
[<Literal>] val PathToSchemaFile: string = "docsSrc/schema.json"
type FromFile = JsonSchemaProvider<...>
val xz1: JsonSchemaProvider<...>
JsonSchemaProvider<...>.Create(?X: string, ?Y: string, ?Z: System.Nullable<int>) : JsonSchemaProvider<...>
argument X: string
argument Z: System.Nullable<int>
val xz2: JsonSchemaProvider<...>
JsonSchemaProvider<...>.Parse(json: string) : JsonSchemaProvider<...>
val ignore: value: 'T -> unit
namespace System
Multiple items
type ArgumentException = inherit SystemException new: unit -> unit + 4 overloads member GetObjectData: info: SerializationInfo * context: StreamingContext -> unit static member ThrowIfNullOrEmpty: argument: string * ?paramName: string -> unit static member ThrowIfNullOrWhiteSpace: argument: string * ?paramName: string -> unit member Message: string member ParamName: string
<summary>The exception that is thrown when one of the arguments provided to a method is not valid.</summary>

--------------------
System.ArgumentException() : System.ArgumentException
System.ArgumentException(message: string) : System.ArgumentException
System.ArgumentException(message: string, innerException: exn) : System.ArgumentException
System.ArgumentException(message: string, paramName: string) : System.ArgumentException
System.ArgumentException(message: string, paramName: string, innerException: exn) : System.ArgumentException
val ex: System.ArgumentException
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
property System.ArgumentException.Message: string with get
<summary>Gets the error message and the parameter name, or only the error message if no parameter name is set.</summary>
<returns>A text string describing the details of the exception. The value of this property takes one of two forms: <list type="table"><listheader><term> Condition</term><description> Value</description></listheader><item><term> The <paramref name="paramName" /> is a null reference (<see langword="Nothing" /> in Visual Basic) or of zero length.</term><description> The <paramref name="message" /> string passed to the constructor.</description></item><item><term> The <paramref name="paramName" /> is not null reference (<see langword="Nothing" /> in Visual Basic) and it has a length greater than zero.</term><description> The <paramref name="message" /> string appended with the name of the invalid parameter.</description></item></list></returns>
[<Literal>] val rangeSchema: string = " { "type": "object", "properties": { "from": { "type": "integer", "minimum": 0 }, "to": { "type": "integer", "maximum": 10 } } }"
type Range = JsonSchemaProvider<...>
JsonSchemaProvider<...>.Create(?from: System.Nullable<int>, ?``to`` : System.Nullable<int>) : JsonSchemaProvider<...>
[<Literal>] val rangeRequiredSchema: string = " { "type": "object", "properties": { "from": { "type": "integer", "minimum": 0 }, "to": { "type": "integer", "maximum": 10 } }, "required": ["from", "to"] }"
type RangeRequired = JsonSchemaProvider<...>
[<Literal>] val nameSchema: string = " { "type": "object", "properties": { "firstName": {"type": "string"}, "middleInitials": {"type": "string"}, "lastName": {"type": "string"} }, "required": ["firstName", "lastName"] }"
type Name = JsonSchemaProvider<...>
val name1: JsonSchemaProvider<...>
JsonSchemaProvider<...>.Create(firstName: string, ?middleInitials: string, lastName: string) : JsonSchemaProvider<...>
property JsonSchemaProvider<...>.firstName: string with get
property JsonSchemaProvider<...>.middleInitials: Option<string> with get
[<Literal>] val cityPosition: string = " { "type": "object", "properties": { "city": {"type": "string"}, "globalPosition": { "type": "object", "properties": { "lat": {"type": "number"}, "lon": {"type": "number"} }, "required": ["lat", "lon"] } }, "required": ["city", "globalPosition"] }"
type CityPosition = JsonSchemaProvider<...>
val position: JsonSchemaProvider<...>.globalPositionObj
type globalPositionObj = inherit NullableJsonValue static member Create: lat: float * lon: float -> globalPositionObj member lat: float member lon: float
JsonSchemaProvider<...>.globalPositionObj.Create(lat: float, lon: float) : JsonSchemaProvider<...>.globalPositionObj
val berlinPosition: JsonSchemaProvider<...>
JsonSchemaProvider<...>.Create(city: string, globalPosition: JsonSchemaProvider<...>.globalPositionObj) : JsonSchemaProvider<...>
val berlinLat: float
property JsonSchemaProvider<...>.globalPosition: JsonSchemaProvider<...>.globalPositionObj with get
property JsonSchemaProvider<...>.globalPositionObj.lat: float with get
[<Literal>] val temperatures: string = " { "type": "object", "properties": { "location": {"type": "string"}, "values": { "type": "array", "items": {"type": "number"} } }, "required": ["location", "values"] }"
type Temperatures = JsonSchemaProvider<...>
val temps: JsonSchemaProvider<...>
JsonSchemaProvider<...>.Create(location: string, values: List<float>) : JsonSchemaProvider<...>
val temp: float
property JsonSchemaProvider<...>.values: List<float> with get
[<Literal>] val table: string = " { "type": "object", "properties": { "cells": { "type": "array", "items": { "type": "array", "items": { "type": "object", "properties": { "align": {"type": "string"}, "content": {"type": "string"} } } } } }, "required": ["cells"] }"
type Table = JsonSchemaProvider<...>
val beginEndTable: JsonSchemaProvider<...>
property JsonSchemaProvider<...>.cells: List<List<JsonSchemaProvider<...>.cellsObj>> with get
Multiple items
module List from Microsoft.FSharp.Collections

--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T member IsEmpty: bool member Item: index: int -> 'T with get ...
val concat: lists: 'T list seq -> 'T list
val map: mapping: ('T -> 'U) -> list: 'T list -> 'U list
val props: JsonSchemaProvider<...>.cellsObj
override NullableJsonValue.ToString: unit -> string
type cellsObj = inherit NullableJsonValue static member Create: ?align: string * ?content: string -> cellsObj member align: Option<string> member content: Option<string>
JsonSchemaProvider<...>.cellsObj.Create(?align: string, ?content: string) : JsonSchemaProvider<...>.cellsObj