Farse 0.2.6

dotnet add package Farse --version 0.2.6
                    
NuGet\Install-Package Farse -Version 0.2.6
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Farse" Version="0.2.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Farse" Version="0.2.6" />
                    
Directory.Packages.props
<PackageReference Include="Farse" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Farse --version 0.2.6
                    
#r "nuget: Farse, 0.2.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Farse@0.2.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Farse&version=0.2.6
                    
Install as a Cake Addin
#tool nuget:?package=Farse&version=0.2.6
                    
Install as a Cake Tool

Build NuGet Version

Farse

Simple parsing library for F# using System.Text.Json.

Inspired by Thoth.Json and its composability.

Farse uses a slightly different syntax, includes a computation expression, and a few custom operators that simplify parsing. It also tries to keep a low overhead while still producing acceptable error messages.

Installation

dotnet add package Farse

Benchmarks

There are some initial benchmarks here.

BenchmarkDotNet v0.15.2, macOS Sequoia 15.6 (24G84) [Darwin 24.6.0]
Apple M1 Pro, 1 CPU, 8 logical and 8 physical cores
.NET SDK 9.0.203
  [Host]     : .NET 9.0.4 (9.0.425.16305), Arm64 RyuJIT AdvSIMD DEBUG
  DefaultJob : .NET 9.0.4 (9.0.425.16305), Arm64 RyuJIT AdvSIMD
| Method                 | Mean      | Ratio | Gen0   | Gen1   | Allocated | Alloc Ratio |
|----------------------- |----------:|------:|-------:|-------:|----------:|------------:|
| System.Text.Json       |  3.743 us |  0.77 | 0.1106 |      - |     696 B |        0.22 |
| System.Text.Json*      |  3.771 us |  0.77 | 0.4082 | 0.0076 |    2562 B |        0.80 |
| Farse                  |  4.877 us |  1.00 | 0.5035 |      - |    3200 B |        1.00 |
| Newtonsoft.Json*       |  6.164 us |  1.26 | 1.5182 | 0.0229 |    9544 B |        2.98 |
| Thoth.System.Text.Json |  8.095 us |  1.66 | 1.5717 | 0.0153 |    9944 B |        3.11 |
| Newtonsoft.Json        |  8.536 us |  1.75 | 2.8229 | 0.1373 |   17720 B |        5.54 |
| Thoth.Json.Net         | 10.423 us |  2.14 | 3.3569 | 0.1526 |   21136 B |        6.61 |

* Serialization

Example

Given the following JSON string.

{
    "id": "c8eae96a-025d-4bc9-88f8-f204e95f2883",
    "name": "Alice",
    "age": null,
    "email": "alice@domain.com",
    "profiles": [
        "01458283-b6e3-4ae7-ae54-a68eb587cdc0",
        "bf00d1e2-ee53-4969-9507-86bed7e96432",
        "927eb20f-cd62-470c-aafc-c3ce6b9248b0"
    ],
    "subscription": {
        "plan": "Pro",
        "isCanceled": false,
        "renewsAt": "2026-12-25T10:30:00Z"
    }
}

And the two (optional) operators.

// Parses a required property.
let (&=) = Parse.req

// Parses an optional property.
let (?=) = Parse.opt

We can create a parser.

open Farse
open Farse.Operators

module User =
    open Parse

    let parser =
        parser {
            let! id = "id" &= UserId.parser
            let! name = "name" &= string
            let! age = "age" ?= Age.parser
            let! email = "email" &= Email.parser
            let! profiles = "profiles" &= set ProfileId.parser

            // Inlined parser example.
            let! subscription = "subscription" &= parser {
                let! plan = "plan" &= Plan.parser
                let! isCanceled = "isCanceled" &= bool
                let! renewsAt = "renewsAt" ?= Parse.instant // Custom parser example.
    
                return {
                    Plan = plan
                    IsCanceled = isCanceled
                    RenewsAt = renewsAt
                }
            }

            // Simple "path" example, which can be very useful
            // when we just want to parse a (few) nested value(s).
            let! _isCanceled = "subscription.isCanceled" &= bool
      
            return {
                Id = id
                Name = name
                Age = age
                Email = email
                Profiles = profiles
                Subscription = subscription
            }
        }

With the following types.

open Farse
open NodaTime

type UserId = UserId of Guid

module UserId =

    let asString (UserId x) =
        string x

    let parser =
        Parse.guid
        |> Parser.map UserId

type Age = Age of byte

module Age =

    let asByte (Age x) = x

    let fromByte age =
        match age with
        | age when age >= 12uy -> Ok <| Age age
        | age -> Error $"Invalid age: %u{age}."

    let parser =
        Parse.byte
        |> Parser.validate fromByte

type Email = Email of string

module Email =

    let asString (Email x) = x

    let fromString str =
        // Some validation.
        Ok <| Email str

    let parser =
        Parse.string
        |> Parser.validate fromString

type ProfileId = ProfileId of Guid

module ProfileId =

    let asString (ProfileId x) =
        string x

    let parser =
        Parse.guid
        |> Parser.map ProfileId

type Plan =
    | Pro
    | Standard
    | Free

module Plan =

    let fromString = function
        | "Pro" -> Ok Pro
        | "Standard" -> Ok Standard
        | "Free" -> Ok Free
        | invalid -> Error $"Invalid plan: %s{invalid}."

    let asString = function
        | Pro -> "Pro"
        | Standard -> "Standard"
        | Free -> "Free"

    let parser =
        Parse.string
        |> Parser.validate fromString

type Subscription = {
    Plan: Plan
    IsCanceled: bool
    RenewsAt: Instant option
}

type User = {
    Id: UserId
    Name: string
    Age: Age option
    Email: Email
    Profiles: ProfileId Set
    Subscription: Subscription
}

Then we can just run the parser.

let user =
    User.parser
    |> Parser.parse json
    |> Result.defaultWith failwith

printf "%s" user.Name

Custom parsers

You can use Parse.custom to create custom parsers.

open Farse
open NodaTime
open NodaTime.Text
open System.Text.Json

module Parse =

    let instant =
        Parse.custom (fun (element:JsonElement) ->
            let string = element.GetString()
            match InstantPattern.General.Parse(string) with
            | result when result.Success -> true, result.Value
            | _ -> false, Instant.MinValue
        ) JsonValueKind.String

Creating JSON

We can also create JSON strings with the Json type.

Note: Use JNum<'a> if you want to be explicit.

JObj [
    "id", JStr <| UserId.asString user.Id
    "name", JStr user.Name
    "age",
        user.Age
        |> Option.map (Age.asByte >> JNum)
        |> JNil
    "email", JStr <| Email.asString user.Email
    "profiles",
        user.Profiles
        |> Seq.map (ProfileId.asString >> JStr)
        |> JArr
    "subscription", JObj [
        "plan", JStr <| Plan.asString user.Subscription.Plan
        "isCanceled", JBit user.Subscription.IsCanceled
        "renewsAt",
            user.Subscription.RenewsAt
            |> Option.map (_.ToString() >> JStr)
            |> JNil
    ]
]

This creates an object, but you can create any type and convert it with Json.asString.

Errors

More examples can be found here.

Error: Could not parse property 'prop'.
Expected: Number, actual: String.
Object:
{
  "prop": "1"
}

Farse doesn't throw exceptions and only catches JsonException, that is thrown when parsing invalid JSON.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.6 10 8/19/2025
0.2.5 12 8/17/2025
0.2.4 94 8/13/2025
0.2.3 92 8/8/2025
0.2.2 189 8/5/2025
0.2.1 155 8/4/2025
0.2.0 92 7/31/2025
0.1.9-alpha 90 7/27/2025
0.1.8-alpha 32 7/18/2025
0.1.7-alpha 112 7/9/2025
0.1.6-alpha 57 7/4/2025
0.1.5-alpha 63 6/20/2025
0.1.4-alpha 124 6/18/2025
0.1.3-alpha 86 5/30/2025
0.1.2-alpha 139 5/16/2025
0.1.1-alpha 134 5/16/2025
0.1.0-alpha 139 5/16/2025