Saon: catchy description here

Saon is an F# library to parse and validate json payloads and query strings. It builds on top of the System.Text.Json and Microsoft.AspNetCore.Http.Features packages.

WARNING: despite the version number, Saon is still in the very early stages. I'm still experimenting with the API since I'm not satisfied how the json converters and validation functions are composed together.

How to get Saon

Saon is ready to download from Nuget. At the moment, there are four package you can download:

  • Saon: this is an umbrella package that includes all the other packages. If you're using Saon in an Asp.Net Core application, you should use this package
  • Saon.Json: this package contains the functions to parse json data
  • Saon.Query: this package contains the functions to parse query strings
  • Saon.Shared: this is the core package that defines the base types and functions

Using with Giraffe

Saon can be easily integrated with Giraffe to provide json and query strings parsing and validation. You can find more information in the tutorial.

1: 
2: 
3: 
4: 
5: 
let webApp =
    choose [
        POST >=> route "/" >=> Helper.bindJson Dto.parseContactDetails Handler.createContact
        GET >=> route "/" >=> Helper.bindQuery Dto.parseLookup Handler.lookupContact
    ]

Parsing json payloads

One of the goals of this library is to enable developers to parse idiomatic json payloads and convert them to strictly typed F# objects.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
type Email = Email of string
type ContactDetails =
    { Name : string
      Email : Email }

let parseEmail =
    Validate.isNotEmptyOrWhitespace
    /> Validate.isEmail
    /> Convert.withFunction Email

let parseContactDetails = jsonObjectParser {
    let! name = Json.property "name" (Json.string /> Validate.isNotEmptyOrWhitespace)
    let! email = Json.property "email" (Json.string /> parseEmail)
    return { Name = name; Email = email }
}

You can find more information about the json module in the tutorial.

Parsing query strings

Saon includes a module to parse query strings represented by the IQueryStringCollection interface.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
type LookupType = Name | Email

let parseLookupType paramName = function
    | "name" -> ParserResult.success Name
    | "email" -> ParserResult.success Email
    | _ -> ParserResult.validationFail "type" paramName "must be one of 'name', 'email'"

type Lookup =
    | Name of string
    | Email of string

let parseLookup = queryParser {
    let! lookupType = Query.parameter "type" (Query.string /> parseLookupType)
    match lookupType with
    | LookupType.LookupName ->
        let! value = Query.parameter "value" Query.string
        return LookupName value
    | LookupType.LookupEmail ->
        let! value = Query.parameter "value" (Query.string /> Validate.isEmail)
        return Email value |> LookupEmail
}

You can find more information about the query module in the tutorial.

Validation and conversion

Saon includes a range of validation and conversion functions. You can use them as building block to build your very own parsers! You can read more in the tutorial.

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

Contributing and License

This library is published under the Apache-2.0 license. You can find a copy of the license in the repository.

The project is hosted on GitHub, where you can report issues and open pull requests.

namespace Saon
namespace Saon.Json
module Operators

from Saon
namespace Saon.Query
namespace System
namespace System.Text
val webApp : obj
type Handler<'T> =
  delegate of obj * 'T -> unit
Multiple items
union case Email.Email: string -> Email

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

--------------------
type string = System.String
type ContactDetails =
  {Name: string;
   Email: Email;}
ContactDetails.Name: string
Multiple items
ContactDetails.Email: Email

--------------------
type Email = | Email of string
val parseEmail : (string -> string -> ParserResult<Email>)
module Validate

from Saon
val isNotEmptyOrWhitespace : propName:string -> value:string -> ParserResult<string>
val isEmail : propName:string -> value:string -> ParserResult<string>
module Convert

from Saon
val withFunction : func:('T -> 'R) -> string -> value:'T -> ParserResult<'R>
val parseContactDetails : obj
Multiple items
module Json

from Saon.Json

--------------------
namespace Saon.Json
type LookupType =
  | Name
  | Email
union case LookupType.Name: LookupType
Multiple items
union case LookupType.Email: LookupType

--------------------
type Email = | Email of string
val parseLookupType : paramName:string -> _arg1:string -> ParserResult<LookupType>
val paramName : 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>
type Lookup =
  | Name of string
  | Email of string
union case Lookup.Name: string -> Lookup
Multiple items
union case Lookup.Email: string -> Lookup

--------------------
type Email = | Email of string
val parseLookup : (AspNetCore.Http.IQueryCollection -> ParserResult<obj>)
val queryParser : ParserBuilder<AspNetCore.Http.IQueryCollection>
val lookupType : LookupType
Multiple items
module Query

from Saon.Query

--------------------
namespace Saon.Query
val parameter : paramName:string -> func:Transformer<Extensions.Primitives.StringValues,'T> -> query:AspNetCore.Http.IQueryCollection -> ParserResult<'T> * AspNetCore.Http.IQueryCollection
val string : propName:string -> value:Extensions.Primitives.StringValues -> ParserResult<string>
val parsePositiveAmount : (string -> string -> ParserResult<decimal>)
val stringToDecimal : propName:string -> value:string -> ParserResult<decimal>
val isGreaterThan : minValue:'T -> propName:string -> value:'T -> ParserResult<'T> (requires comparison)
val parseSize : (string -> string -> ParserResult<obj>)
val parsePrice : (string -> string -> ParserResult<obj>)