Farse 0.6.0

dotnet add package Farse --version 0.6.0
                    
NuGet\Install-Package Farse -Version 0.6.0
                    
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.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Farse" Version="0.6.0" />
                    
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.6.0
                    
#r "nuget: Farse, 0.6.0"
                    
#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.6.0
                    
#: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.6.0
                    
Install as a Cake Addin
#tool nuget:?package=Farse&version=0.6.0
                    
Install as a Cake Tool

Build NuGet

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.3, macOS 26.0 (25A354) [Darwin 25.0.0]
Apple M1 Pro, 1 CPU, 8 logical and 8 physical cores
.NET SDK 9.0.305
  [Host]     : .NET 9.0.9 (9.0.9, 9.0.925.41916), Arm64 RyuJIT armv8.0-a DEBUG
  DefaultJob : .NET 9.0.9 (9.0.9, 9.0.925.41916), Arm64 RyuJIT armv8.0-a
| Method                 | Mean     | Ratio | Gen0    | Gen1    | Allocated | Alloc Ratio |
|----------------------- |---------:|------:|--------:|--------:|----------:|------------:|
| System.Text.Json       | 109.1 us |  0.83 |  4.1504 |       - |  25.85 KB |        0.60 |
| System.Text.Json*      | 112.1 us |  0.86 | 13.0615 |  1.7090 |  80.19 KB |        1.86 |
| Farse                  | 130.9 us |  1.00 |  6.8359 |       - |  43.02 KB |        1.00 |
| Thoth.System.Text.Json | 247.5 us |  1.89 | 55.1758 | 18.0664 | 338.76 KB |        7.87 |
| Newtonsoft.Json*       | 248.1 us |  1.89 | 58.5938 |  5.8594 | 365.01 KB |        8.48 |
| Newtonsoft.Json        | 274.0 us |  2.09 | 75.6836 | 22.9492 | 464.07 KB |       10.79 |
| Thoth.Json.Net         | 365.5 us |  2.79 | 94.7266 | 44.9219 | 581.86 KB |       13.52 |

* Serialization

Example

The complete example can be found here.

Given the JSON string.

{
    "id": "c8eae96a-025d-4bc9-88f8-f204e95f2883",
    "name": "Alice",
    "age": null,
    "email": "alice@domain.com",
    "profiles": [
        "01458283-b6e3-4ae7-ae54-a68eb587cdc0",
        "927eb20f-cd62-470c-aafc-c3ce6b9248b0",
        "bf00d1e2-ee53-4969-9507-86bed7e96432"
    ],
    "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 simple parser.

open Farse
open Farse.Operators

module User =
    open Parse

    let parser =
        parser {
            let! id = "id" &= guid |> Parser.map UserId
            and! name = "name" &= string
            and! age = "age" ?= valid byte Age.fromByte
            and! email = "email" &= valid string Email.fromString
            and! profiles = "profiles" &= set profileId // Custom parser example.

            // Inlined parser example.
            and! subscription = "subscription" &= parser {
                let! plan = "plan" &= valid string Plan.fromString
                and! isCanceled = "isCanceled" &= bool
                and! renewsAt = "renewsAt" ?= instant // Custom parser example.

                return {
                    Plan = plan
                    IsCanceled = isCanceled
                    RenewsAt = renewsAt
                }
            }

            // "Path" example, which can be very useful
            // when we just want to parse a (few) nested value(s).
            and! _isCanceled = "subscription.isCanceled" &= bool

            return {
                Id = id
                Name = name
                Age = age
                Email = email
                Profiles = profiles
                Subscription = subscription
            }
        }

Note: In this example, our parsers are defined under the same module name as included parsers.

With the following types.

type UserId = UserId of Guid

module UserId =

    let asString (UserId x) =
        string x

type Age = Age of byte

module Age =

    let fromByte = function
        | age when age >= 12uy -> Ok <| Age age
        | age -> Error $"Invalid age '%u{age}'."
        
    let asByte (Age x) = x

type Email = Email of string

module Email =

    let fromString =
        // Some validation.
        Email >> Ok
        
    let asString (Email x) = x

type ProfileId = ProfileId of Guid

module ProfileId =

    let asString (ProfileId x) =
        string x

type Plan =
    | Pro
    | Standard
    | Free

module Plan =

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

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

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

Parse.custom can be used to build parsers for third-party types, or to just avoid unnecessary operations.

open Farse

module Parse =

    let profileId =
        Parse.custom (fun element ->
            match element.TryGetGuid() with
            | true, guid -> Ok <| ProfileId guid
            | _ -> Error None // No details.
        ) ExpectedKind.String

    let instant =
        Parse.custom (fun element ->
            let string = element.GetString()
            match InstantPattern.General.Parse(string) with
            | result when result.Success -> Ok result.Value
            | result -> Error <| Some result.Exception.Message // Added as details.
        ) ExpectedKind.String

Note: Custom parsers produce detailed error messages when validation fails, see errors.

Creating JSON

We can create JSON strings with Json.

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

open Farse

module User =
    
    let asJson user =
        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
                ]
        ]

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

Errors

More examples can be found here.

Object
Error: Could not parse property 'renewsAt'.
Message: Tried parsing '202612-25T10:30:00Z' to Instant. Details: The value string [...]
Object:
{
    "plan": "Pro",
    "isCanceled": false,
    "renewsAt": "202612-25T10:30:00Z"
}
Array
Error: Could not parse property 'profiles[1]'.
Message: Tried parsing '927eb20f-cd62-470c-aafc-c3ce6b9248' to ProfileId.
Array:
[
    "01458283-b6e3-4ae7-ae54-a68eb587cdc0",
    "927eb20f-cd62-470c-aafc-c3ce6b9248",
    "bf00d1e2-ee53-4969-9507-86bed7e9643c"
]

Note: Farse does not throw exceptions unless something unexpected occurs.

Product Compatible and additional computed target framework versions.
.NET 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.6.0 187 11/9/2025
0.5.2 161 10/5/2025
0.5.1 165 9/25/2025
0.5.0 220 9/21/2025
0.4.0 146 9/13/2025
0.3.0 212 8/27/2025
0.2.7 78 8/23/2025
0.2.6 147 8/19/2025
0.2.5 124 8/17/2025
0.2.4 171 8/13/2025
0.2.3 161 8/8/2025
0.2.2 256 8/5/2025
0.2.1 206 8/4/2025
0.2.0 149 7/31/2025
0.1.9-alpha 158 7/27/2025
0.1.8-alpha 103 7/18/2025
0.1.7-alpha 154 7/9/2025
0.1.6-alpha 113 7/4/2025
0.1.5-alpha 120 6/20/2025
0.1.4-alpha 167 6/18/2025
0.1.3-alpha 137 5/30/2025
0.1.2-alpha 193 5/16/2025
0.1.1-alpha 185 5/16/2025
0.1.0-alpha 192 5/16/2025