Header menu logo FSharp.Data

Using JSON Schema with the JSON Type Provider

The JSON Type Provider allows you to use JSON Schema to provide statically typed access to JSON documents, similar to how the XML Type Provider supports XML Schema.

Basic Usage with JSON Schema

Let's start with a basic JSON Schema example:

let personSchema = """
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "firstName": {
      "type": "string",
      "description": "The person's first name."
    },
    "lastName": {
      "type": "string",
      "description": "The person's last name."
    },
    "age": {
      "description": "Age in years",
      "type": "integer",
      "minimum": 0
    },
    "email": {
      "type": "string",
      "format": "email"
    },
    "phoneNumbers": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": ["home", "work", "mobile"]
          },
          "number": {
            "type": "string"
          }
        },
        "required": ["type", "number"]
      }
    }
  },
  "required": ["firstName", "lastName"]
}
"""

// Create a type based on the schema
[<Literal>]
let PersonSchemaLiteral = """
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "firstName": {
      "type": "string",
      "description": "The person's first name."
    },
    "lastName": {
      "type": "string",
      "description": "The person's last name."
    },
    "age": {
      "description": "Age in years",
      "type": "integer",
      "minimum": 0
    },
    "email": {
      "type": "string",
      "format": "email"
    },
    "phoneNumbers": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": ["home", "work", "mobile"]
          },
          "number": {
            "type": "string"
          }
        },
        "required": ["type", "number"]
      }
    }
  },
  "required": ["firstName", "lastName"]
}
"""

type Person = JsonProvider<Schema=PersonSchemaLiteral>

// Parse a JSON document that conforms to the schema
let person = Person.Parse("""
{
  "firstName": "John",
  "lastName": "Smith",
  "age": 42,
  "email": "john.smith@example.com",
  "phoneNumbers": [
    {
      "type": "home",
      "number": "555-1234"
    },
    {
      "type": "mobile",
      "number": "555-6789"
    }
  ]
}
""")

// Access the strongly typed properties
printfn "Name: %s %s" person.FirstName person.LastName
printfn "Age: %A" person.Age
printfn "Email: %A" person.Email
printfn "Phone: %s" person.PhoneNumbers.[0].Number

Using Schema Files

You can also load a JSON Schema from a file:

// Assuming you have a schema file:
// type Product = JsonProvider<Schema="schemas/product.json">

Validating JSON Against Schema

When using the JSON Provider with the Schema parameter, data validation occurs automatically at parse time based on the schema rules:

Here's how validation works:

// Valid JSON that conforms to the schema
let validPerson = Person.Parse("""
{
  "firstName": "Jane",
  "lastName": "Doe",
  "age": 35,
  "email": "jane.doe@example.com"
}
""")
printfn "Valid JSON: %s %s" validPerson.FirstName validPerson.LastName

// Invalid JSON that violates schema rules will cause an exception
// Let's use try-catch to demonstrate validation errors:
let invalidJson = """
{
  "firstName": "John",
  "age": -5
}
"""

// In a real project when using the Schema parameter, the JsonProvider would validate
// against the schema rules. For the purposes of this demonstration, let's manually
// validate the JSON against the schema:

// Create a JSON value from the invalid JSON
let jsonValue = JsonValue.Parse(invalidJson)

// Check required fields from the schema
if jsonValue.TryGetProperty("lastName").IsNone then
    printfn "Schema validation failed: missing required property 'lastName'"

// Check numeric constraints from the schema
if jsonValue.TryGetProperty("age").IsSome &&
   jsonValue.["age"].AsInteger() < 0 then
    printfn "Schema validation failed: 'age' must be non-negative"

Schema Constraints and Validation

JSON Schema supports various constraints that are validated:

String Constraints

{
  "type": "string",
  "minLength": 3,
  "maxLength": 50,
  "pattern": "^[A-Z][a-z]+$"
}

Numeric Constraints

{
  "type": "number",
  "minimum": 0,
  "maximum": 100
}

Array Constraints

{
  "type": "array",
  "items": {
    "type": "string"
  },
  "minItems": 1,
  "maxItems": 10
}

Object Constraints

{
  "type": "object",
  "required": ["id", "name"],
  "properties": {
    "id": { "type": "integer" },
    "name": { "type": "string" }
  }
}

Working with Schema References

JSON Schema allows references to reuse schema definitions:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "address": {
      "type": "object",
      "properties": {
        "street": { "type": "string" },
        "city": { "type": "string" },
        "zipCode": { "type": "string" }
      },
      "required": ["street", "city", "zipCode"]
    }
  },
  "type": "object",
  "properties": {
    "billingAddress": { "$ref": "#/definitions/address" },
    "shippingAddress": { "$ref": "#/definitions/address" }
  }
}

Advantages of Using JSON Schema

  1. Documentation: Schema provides documentation on what properties are available.
  2. Validation: Schema enforces constraints on data types, required properties, etc.
  3. Type Safety: Strong typing to prevent errors when working with JSON data.
  4. Discoverability: Better IntelliSense in your IDE.
  5. Consistency: Ensure all documents follow the same structure.
  6. Contract First Development: Define your data contract before implementation.

JSON Schema Features Supported

The JSON Schema support in FSharp.Data includes:

Requirements and Limitations

Using JSON Schema in Your Project

To use JSON Schema with the JSON Type Provider:

  1. Define your schema (in a file or as a string)
  2. Create a type using JsonProvider<Schema="path-to-schema.json"> or JsonProvider<Schema=schemaString>
  3. Use the generated type to parse and work with your JSON data
  4. Optionally use the validation functions for runtime validation

Complete Example with Nested Objects

Here's a more complex example with nested objects:

let orderSchema = """
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "orderId": {
      "type": "string",
      "pattern": "^ORD-[0-9]{6}$"
    },
    "customer": {
      "type": "object",
      "properties": {
        "id": { "type": "integer" },
        "name": { "type": "string" },
        "email": { "type": "string", "format": "email" }
      },
      "required": ["id", "name"]
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "productId": { "type": "string" },
          "name": { "type": "string" },
          "quantity": { "type": "integer", "minimum": 1 },
          "price": { "type": "number", "minimum": 0 }
        },
        "required": ["productId", "quantity", "price"]
      },
      "minItems": 1
    },
    "totalAmount": { "type": "number", "minimum": 0 },
    "orderDate": { "type": "string", "format": "date-time" }
  },
  "required": ["orderId", "customer", "items", "totalAmount", "orderDate"]
}
"""

// Create a type based on the order schema
[<Literal>]
let OrderSchemaLiteral = """
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "orderId": {
      "type": "string",
      "pattern": "^ORD-[0-9]{6}$"
    },
    "customer": {
      "type": "object",
      "properties": {
        "id": { "type": "integer" },
        "name": { "type": "string" },
        "email": { "type": "string", "format": "email" }
      },
      "required": ["id", "name"]
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "productId": { "type": "string" },
          "name": { "type": "string" },
          "quantity": { "type": "integer", "minimum": 1 },
          "price": { "type": "number", "minimum": 0 }
        },
        "required": ["productId", "quantity", "price"]
      },
      "minItems": 1
    },
    "totalAmount": { "type": "number", "minimum": 0 },
    "orderDate": { "type": "string", "format": "date-time" }
  },
  "required": ["orderId", "customer", "items", "totalAmount", "orderDate"]
}
"""

type Order = JsonProvider<Schema=OrderSchemaLiteral>

let order = Order.Parse("""
{
  "orderId": "ORD-123456",
  "customer": {
    "id": 1001,
    "name": "Alice Smith",
    "email": "alice@example.com"
  },
  "items": [
    {
      "productId": "PROD-001",
      "name": "Laptop",
      "quantity": 1,
      "price": 1299.99
    },
    {
      "productId": "PROD-002",
      "name": "Mouse",
      "quantity": 2,
      "price": 25.99
    }
  ],
  "totalAmount": 1351.97,
  "orderDate": "2023-10-01T12:00:00Z"
}
""")

printfn "Order: %s" order.OrderId
printfn "Customer: %s" order.Customer.Name
printfn "Items: %d" order.Items.Length
printfn "Total: %.2f" order.TotalAmount
printfn "Date: %A" order.OrderDate

Summary

The JSON Schema support in FSharp.Data provides a powerful way to work with strongly-typed JSON data based on schema definitions. It combines the benefits of static typing with the flexibility of JSON, making it an excellent choice for working with well-defined JSON APIs and data structures.

namespace System
namespace System.IO
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
Multiple items
namespace FSharp.Data

--------------------
namespace Microsoft.FSharp.Data
val personSchema: string
Multiple items
type LiteralAttribute = inherit Attribute new: unit -> LiteralAttribute

--------------------
new: unit -> LiteralAttribute
[<Literal>] val PersonSchemaLiteral: string = " { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "firstName": { "type": "string", "description": "The person's first name." }, "lastName": { "type": "string", "description": "The person's last name." }, "age": { "description": "Age in years", "type": "integer", "minimum": 0 }, "email": { "type": "string", "format": "email" }, "phoneNumbers": { "type": "array", "items": { "type": "object", "properties": { "type": { "type": "string", "enum": ["home", "work", "mobile"] }, "number": { "type": "string" } }, "required": ["type", "number"] } } }, "required": ["firstName", "lastName"] } "
type Person = JsonProvider<...>
type JsonProvider
<summary>Typed representation of a JSON document.</summary> <param name='Sample'>Location of a JSON sample file or a string containing a sample JSON document.</param> <param name='SampleIsList'>If true, sample should be a list of individual samples for the inference.</param> <param name='RootName'>The name to be used to the root type. Defaults to `Root`.</param> <param name='Culture'>The culture used for parsing numbers and dates. Defaults to the invariant culture.</param> <param name='Encoding'>The encoding used to read the sample. You can specify either the character set name or the codepage number. Defaults to UTF8 for files, and to ISO-8859-1 the for HTTP requests, unless `charset` is specified in the `Content-Type` response header.</param> <param name='ResolutionFolder'>A directory that is used when resolving relative file references (at design time and in hosted execution).</param> <param name='EmbeddedResource'>When specified, the type provider first attempts to load the sample from the specified resource (e.g. 'MyCompany.MyAssembly, resource_name.json'). This is useful when exposing types generated by the type provider.</param> <param name='InferTypesFromValues'> This parameter is deprecated. Please use InferenceMode instead. If true, turns on additional type inference from values. (e.g. type inference infers string values such as "123" as ints and values constrained to 0 and 1 as booleans.)</param> <param name='PreferDictionaries'>If true, json records are interpreted as dictionaries when the names of all the fields are inferred (by type inference rules) into the same non-string primitive type.</param> <param name='InferenceMode'>Possible values: | NoInference -> Inference is disabled. All values are inferred as the most basic type permitted for the value (i.e. string or number or bool). | ValuesOnly -> Types of values are inferred from the Sample. Inline schema support is disabled. This is the default. | ValuesAndInlineSchemasHints -> Types of values are inferred from both values and inline schemas. Inline schemas are special string values that can define a type and/or unit of measure. Supported syntax: typeof&lt;type&gt; or typeof{type} or typeof&lt;type&lt;measure&gt;&gt; or typeof{type{measure}}. Valid measures are the default SI units, and valid types are <c>int</c>, <c>int64</c>, <c>bool</c>, <c>float</c>, <c>decimal</c>, <c>date</c>, <c>datetimeoffset</c>, <c>timespan</c>, <c>guid</c> and <c>string</c>. | ValuesAndInlineSchemasOverrides -> Same as ValuesAndInlineSchemasHints, but value inferred types are ignored when an inline schema is present. </param> <param name='Schema'>Location of a JSON Schema file or a string containing a JSON Schema document. When specified, Sample and SampleIsList must not be used.</param>
val person: JsonProvider<...>.Root
JsonProvider<...>.Parse(text: string) : JsonProvider<...>.Root
Parses the specified JSON Schema string
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
property JsonProvider<...>.Root.FirstName: string with get
property JsonProvider<...>.Root.LastName: string with get
property JsonProvider<...>.Root.Age: Option<int> with get
property JsonProvider<...>.Root.Email: Option<string> with get
property JsonProvider<...>.Root.PhoneNumbers: JsonProvider<...>.Record array with get
val validPerson: JsonProvider<...>.Root
val invalidJson: string
val jsonValue: JsonValue
type JsonValue = | String of string | Number of decimal | Float of float | Record of properties: (string * JsonValue) array | Array of elements: JsonValue array | Boolean of bool | Null member Equals: JsonValue * IEqualityComparer -> bool member Request: url: string * [<Optional>] ?httpMethod: string * [<Optional>] ?headers: (string * string) seq -> HttpResponse member RequestAsync: url: string * [<Optional>] ?httpMethod: string * [<Optional>] ?headers: (string * string) seq -> Async<HttpResponse> member ToString: saveOptions: JsonSaveOptions * ?indentationSpaces: int -> string + 2 overloads member WriteTo: w: TextWriter * saveOptions: JsonSaveOptions * ?indentationSpaces: int -> unit static member AsyncLoad: uri: string * [<Optional>] ?encoding: Encoding -> Async<JsonValue> static member Load: stream: Stream -> JsonValue + 2 overloads static member Parse: text: string -> JsonValue static member ParseMultiple: text: string -> JsonValue seq static member TryParse: text: string -> JsonValue option
<summary> Represents a JSON value. Large numbers that do not fit in the Decimal type are represented using the Float case, while smaller numbers are represented as decimals to avoid precision loss. </summary>
static member JsonValue.Parse: text: string -> JsonValue
static member JsonExtensions.TryGetProperty: x: JsonValue * propertyName: string -> JsonValue option
val orderSchema: string
[<Literal>] val OrderSchemaLiteral: string = " { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "orderId": { "type": "string", "pattern": "^ORD-[0-9]{6}$" }, "customer": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "email": { "type": "string", "format": "email" } }, "required": ["id", "name"] }, "items": { "type": "array", "items": { "type": "object", "properties": { "productId": { "type": "string" }, "name": { "type": "string" }, "quantity": { "type": "integer", "minimum": 1 }, "price": { "type": "number", "minimum": 0 } }, "required": ["productId", "quantity", "price"] }, "minItems": 1 }, "totalAmount": { "type": "number", "minimum": 0 }, "orderDate": { "type": "string", "format": "date-time" } }, "required": ["orderId", "customer", "items", "totalAmount", "orderDate"] } "
type Order = JsonProvider<...>
val order: JsonProvider<...>.Root
property JsonProvider<...>.Root.OrderId: string with get
property JsonProvider<...>.Root.Customer: JsonProvider<...>.Record with get
property JsonProvider<...>.Record.Name: string with get
property JsonProvider<...>.Root.Items: JsonProvider<...>.Record2 array with get
property Array.Length: int with get
<summary>Gets the total number of elements in all the dimensions of the <see cref="T:System.Array" />.</summary>
<exception cref="T:System.OverflowException">The array is multidimensional and contains more than <see cref="F:System.Int32.MaxValue">Int32.MaxValue</see> elements.</exception>
<returns>The total number of elements in all the dimensions of the <see cref="T:System.Array" />; zero if there are no elements in the array.</returns>
property JsonProvider<...>.Root.TotalAmount: decimal with get
property JsonProvider<...>.Root.OrderDate: DateTime with get

Type something to start searching.