Tutorial: Parsing json data

In this tutorial we show how to parse and validate json data. We will consider an example HTTP endpoint to create orders on a fictional exchange.

The exchange accepts market and limit orders. Both order types have the following properties:

  • client_id: an optional id assigned by the client
  • type: market or limit. Defaults to limit
  • side: buy or sell
  • size: the order size. To avoid floating point errors, this value is sent as a string
  • product: the product being sold or bought

Limit orders expand market orders to include the following properties:

  • price: the limit price
  • time_in_force: optional, can be gtc or fok. Defaults to gtc

A json payload for a market order looks like the following:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
{
  "type": "market",
  "client_id": "my-id",
  "side": "buy",
  "size": "120.0",
  "product": "GBP-EUR"
}

While one for a limit order looks like:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
{
  "client_id": "my-id",
  "side": "buy",
  "size": "120.0",
  "price": "1.15",
  "product": "GBP-EUR"
}

Initialisation

For this tutorial, we will be using the System.Text.Json package to parse json documents, and Saon and Saon.Json to parse and validate the data.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
#I "../../src/Saon/bin/Release/netstandard2.0/"
#r "Saon.Shared.dll"
#r "Saon.Json.dll"
#r "Saon.Query.dll"
#r "Saon.dll"
open Saon
open Saon.Json
open Saon.Operators
open Saon.Query
open System.Text.Json

Domain Types

We start by modeling the domain. We try to avoid using types such as string or decimal for our fields to enforce stricter type checking by the compiler.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
type OrderId = OrderId of string
type Side = Buy | Sell
type ProductId = ProductId of string
type Size = Size of decimal
type Price = Price of decimal
type TimeInForce = GTC | FOK

type LimitOrder =
    { ClientOrderId : OrderId option
      Side : Side
      ProductId : ProductId
      Size : Size
      Price : Price
      TimeInForce : TimeInForce }

type MarketOrder =
    { ClientOrderId : OrderId option
      Side : Side
      ProductId : ProductId
      Size : Size }

type OrderType = Limit | Market

type Order =
    | Market of MarketOrder
    | Limit of LimitOrder

Validation and Type Conversion

We define several utility functions to convert from json types (such as strings and numbers) to F# sharp types.

A conversion function from value of type 'T to value of type 'R has a signature like string -> 'T -> ParserResult<'R>, where the first parameter is the property name and the second is the value being converted. The function returns ParserResult.Success if the conversion was successful, otherwise it returns ParserResult.ValidationFailed with more information about the error.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
let parseOrderType propName = function
    | "limit" -> ParserResult.success OrderType.Limit
    | "market" -> ParserResult.success OrderType.Market
    | _ -> ParserResult.validationFail "orderType" propName "must be 'limit' or 'market'"

let parseSide propName = function
    | "buy" -> ParserResult.success Buy
    | "sell" -> ParserResult.success Sell
    | _ -> ParserResult.validationFail "side" propName "must be 'buy' or 'sell'"

let parseTimeInForce propName = function
    | "gtc" -> ParserResult.success GTC
    | "fok" -> ParserResult.success FOK
    | _ -> ParserResult.validationFail "timeInForce" propName "must be 'gtc' or 'fok'"

let parsePositiveAmount = Convert.stringToDecimal /> Validate.isGreaterThan 0m
let parseSize = parsePositiveAmount /> Convert.withFunction Size
let parsePrice = parsePositiveAmount /> Convert.withFunction Price

To build json objects parsers, we can use the jsonObjectParser computational expression.

We use the Json.property and Json.optionalProperty functions to get and parse required and optional properties of our json object. The first parameter is the name of the property, while the second parameter is a function that converts from JsonElement to our type.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
let parseLimitOrder = jsonObjectParser {
    let! clientId = Json.optionalProperty "client_id" (Json.string /> Convert.withFunction OrderId)
    let! product = Json.property "product" (Json.string /> Convert.withFunction ProductId)
    let! side = Json.property "side" (Json.string /> parseSide)
    let! size = Json.property "size" (Json.string /> parseSize)
    let! price = Json.property "price" (Json.string /> parsePrice)
    let! tif = Json.optionalProperty "time_in_force" (Json.string /> parseTimeInForce)
    return
      { ClientOrderId = clientId
        ProductId = product
        Side = side
        Size = size
        Price = price
        TimeInForce = Option.defaultValue GTC tif }
}

let parseMarketOrder = jsonObjectParser {
    let! clientId = Json.optionalProperty "client_id" (Json.string /> Convert.withFunction OrderId)
    let! product = Json.property "product" (Json.string /> Convert.withFunction ProductId)
    let! side = Json.property "side" (Json.string /> parseSide)
    let! size = Json.property "size" (Json.string /> parseSize)
    return
      { ClientOrderId = clientId
        ProductId = product
        Side = side
        Size = size }
}

Our payload includes the fields for market and limit orders in the root json object. Saon allows us to pass the current JsonElement to an object sub parser by using the Json.embeddedObject function.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
let parseOrder = jsonObjectParser {
    let! orderType = Json.optionalProperty "type" (Json.string /> parseOrderType)
    match orderType with
    | Some OrderType.Limit | None ->
        let! order = Json.embeddedObject parseLimitOrder
        return Limit order
    | Some OrderType.Market ->
        let! order = Json.embeddedObject parseMarketOrder
        return Market order
}

Using the parser

We are now ready to test the parser. We tart by defining the json payload and parsing it into a JsonDocument object, then we call parseOrder to parse it to obtain a ParserResult<Order>.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
let parseOrderPayload (payload : string) =
    let document = JsonDocument.Parse(payload)
    parseOrder document.RootElement

let marketOrderPayload = """
{
  "type": "market",
  "client_id": "my-id",
  "side": "buy",
  "size": "120.0",
  "price": "1.15",
  "product": "GBP-EUR"
}
"""
let marketOrder = parseOrderPayload marketOrderPayload

let limitOrderPayload = """
{
  "client_id": "my-id",
  "side": "buy",
  "size": "120.0",
  "price": "1.15",
  "product": "GBP-EUR"
}
"""
let limitOrder = parseOrderPayload limitOrderPayload

If the json payload is malformed, the parser will return a ValidationFailed result with more information about the error.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
let badOrderTypePayload = """
{
  "type": "invalid",
  "client_id": "my-id",
  "side": "buy",
  "size": "120.0",
  "price": "1.15",
  "product": "GBP-EUR"
}
"""
parseOrderPayload badOrderTypePayload


let sizeIsNotANumber = """
{
  "type": "limit",
  "client_id": "my-id",
  "side": "buy",
  "size": "12O",
  "price": "1.15",
  "product": "GBP-EUR"
}
"""
parseOrderPayload sizeIsNotANumber

More about the json module

You can find more information about the Json module in the API reference section.

namespace Saon
namespace Saon.Json
module Operators

from Saon
namespace Saon.Query
namespace System
namespace System.Text
Multiple items
union case OrderId.OrderId: string -> OrderId

--------------------
type OrderId = | OrderId of string
Multiple items
val string : value:'T -> string

--------------------
type string = System.String
type Side =
  | Buy
  | Sell
union case Side.Buy: Side
union case Side.Sell: Side
Multiple items
union case ProductId.ProductId: string -> ProductId

--------------------
type ProductId = | ProductId of string
Multiple items
union case Size.Size: decimal -> Size

--------------------
type Size = | Size of decimal
Multiple items
val decimal : value:'T -> decimal (requires member op_Explicit)

--------------------
type decimal = System.Decimal

--------------------
type decimal<'Measure> = decimal
Multiple items
union case Price.Price: decimal -> Price

--------------------
type Price = | Price of decimal
type TimeInForce =
  | GTC
  | FOK
union case TimeInForce.GTC: TimeInForce
union case TimeInForce.FOK: TimeInForce
type LimitOrder =
  {ClientOrderId: OrderId option;
   Side: Side;
   ProductId: ProductId;
   Size: Size;
   Price: Price;
   TimeInForce: TimeInForce;}
LimitOrder.ClientOrderId: OrderId option
type 'T option = Option<'T>
Multiple items
LimitOrder.Side: Side

--------------------
type Side =
  | Buy
  | Sell
Multiple items
LimitOrder.ProductId: ProductId

--------------------
type ProductId = | ProductId of string
Multiple items
LimitOrder.Size: Size

--------------------
type Size = | Size of decimal
Multiple items
LimitOrder.Price: Price

--------------------
type Price = | Price of decimal
Multiple items
LimitOrder.TimeInForce: TimeInForce

--------------------
type TimeInForce =
  | GTC
  | FOK
type MarketOrder =
  {ClientOrderId: OrderId option;
   Side: Side;
   ProductId: ProductId;
   Size: Size;}
MarketOrder.ClientOrderId: OrderId option
Multiple items
MarketOrder.Side: Side

--------------------
type Side =
  | Buy
  | Sell
Multiple items
MarketOrder.ProductId: ProductId

--------------------
type ProductId = | ProductId of string
Multiple items
MarketOrder.Size: Size

--------------------
type Size = | Size of decimal
type OrderType =
  | Limit
  | Market
union case OrderType.Limit: OrderType
union case OrderType.Market: OrderType
type Order =
  | Market of MarketOrder
  | Limit of LimitOrder
union case Order.Market: MarketOrder -> Order
union case Order.Limit: LimitOrder -> Order
val parseOrderType : propName:string -> _arg1:string -> ParserResult<OrderType>
val propName : string
Multiple items
module ParserResult

from Saon

--------------------
type ParserResult<'T> =
  | ParsingFailed of field: string option * message: string
  | ValidationFailed of ValidationFailedMap
  | Success of 'T
val success : value:'a -> ParserResult<'a>
val validationFail : typ:string -> propName:string -> msg:string -> ParserResult<'a>
val parseSide : propName:string -> _arg1:string -> ParserResult<Side>
val parseTimeInForce : propName:string -> _arg1:string -> ParserResult<TimeInForce>
val parsePositiveAmount : (string -> string -> ParserResult<decimal>)
module Convert

from Saon
val stringToDecimal : propName:string -> value:string -> ParserResult<decimal>
module Validate

from Saon
val isGreaterThan : minValue:'T -> propName:string -> value:'T -> ParserResult<'T> (requires comparison)
val parseSize : (string -> string -> ParserResult<Size>)
val withFunction : func:('T -> 'R) -> string -> value:'T -> ParserResult<'R>
val parsePrice : (string -> string -> ParserResult<Price>)
val parseLimitOrder : obj
Multiple items
module Json

from Saon.Json

--------------------
namespace Saon.Json
module Option

from Microsoft.FSharp.Core
val defaultValue : value:'T -> option:'T option -> 'T
val parseMarketOrder : obj
val parseOrder : (obj -> obj)
union case Option.Some: Value: 'T -> Option<'T>
union case Option.None: Option<'T>
val parseOrderPayload : payload:string -> obj
val payload : string
val document : obj
val marketOrderPayload : string
val marketOrder : obj
val limitOrderPayload : string
val limitOrder : obj
val badOrderTypePayload : string
val sizeIsNotANumber : string