Farse 0.7.1

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

Benchmarks

The benchmarks can be found here.

BenchmarkDotNet v0.15.8, macOS Tahoe 26.2 (25C56) [Darwin 25.2.0]
Apple M1 Pro, 1 CPU, 8 logical and 8 physical cores
.NET SDK 10.0.100
  [Host]     : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a DEBUG
  DefaultJob : .NET 10.0.0 (10.0.0, 10.0.25.52411), Arm64 RyuJIT armv8.0-a
| Method                 | Mean     | Ratio | Gen0    | Gen1    | Allocated | Alloc Ratio |
|----------------------- |---------:|------:|--------:|--------:|----------:|------------:|
| System.Text.Json       | 108.0 us |  0.85 |  4.1504 |       - |  25.85 KB |        0.60 |
| System.Text.Json*      | 108.8 us |  0.86 | 12.9395 |  1.5869 |  79.97 KB |        1.86 |
| Farse                  | 127.1 us |  1.00 |  6.8359 |       - |  43.02 KB |        1.00 |
| Newtonsoft.Json*       | 193.8 us |  1.53 | 42.7246 |  5.8594 | 262.27 KB |        6.10 |
| Thoth.System.Text.Json | 215.0 us |  1.69 | 55.1758 | 18.3105 | 338.76 KB |        7.87 |
| Newtonsoft.Json        | 223.3 us |  1.76 | 75.6836 | 23.9258 | 464.07 KB |       10.79 |
| Thoth.Json.Net         | 308.1 us |  2.42 | 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 (&=) = Prop.req

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

We can create this 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" ?= byte |> Parser.validate Age.fromByte
            and! email = "email" &= string |> Parser.validate Email.fromString
            and! profiles = "profiles" &= set profileId // Custom parser example.

            // Inlined parser example.
            and! subscription = "subscription" &= parser {
                let! plan = "plan" &= string |> Parser.validate 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: Our custom 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 =

    [<Literal>]
    let private MinAge = 12uy

    let fromByte = function
        | age when age >= MinAge -> Ok <| Age age
        | _ -> Error $"The minimum age is '%u{MinAge}'."
        
    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 $"Plan '%s{str}' not found."

    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

Validation

There are a few different options available depending on your use case.

Parser.validate

Passes along the error string from the validation function.

let! age = "age" ?= byte |> Parser.validate Age.fromByte
The minimum age is '12'.
Parse.valid

Produces detailed error messages when validation fails.

let! age = "age" ?= valid byte Age.fromByte
Error: Could not parse property 'age'.
Message: Tried parsing '10' to Age.
Details: The minimum age is '12'.
{
    "id": "c8eae96a-025d-4bc9-88f8-f204e95f2883",
    "name": "Alice",
    "age": 10
}
Parse.custom

Produces detailed error messages when validation fails.

let! age = "age" ?= age
Error: Could not parse property 'age'.
Message: Tried parsing '10' to Age.
Details: The minimum age is '12'.
{
    "id": "c8eae96a-025d-4bc9-88f8-f204e95f2883",
    "name": "Alice",
    "age": 10
}

Creating JSON

We can create JSON strings with the Json type.

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
                |> JNum.nil Age.asByte
            "email", JStr <| Email.asString user.Email
            "profiles",
                user.Profiles
                |> JStr.arr ProfileId.asString 
            "subscription",
                JObj [
                    "plan", JStr <| Plan.asString user.Subscription.Plan
                    "isCanceled", JBit user.Subscription.IsCanceled
                    "renewsAt",
                        user.Subscription.RenewsAt
                        |> JStr.nil _.ToString()
                ]
        ]

Note: Use JNum<'a> and JNum.nil<'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 does not [...]
{
    "plan": "Pro",
    "isCanceled": false,
    "renewsAt": "202612-25T10:30:00Z"
}
Array
Error: Could not parse property 'profiles[1]'.
Message: Tried parsing '927eb20f-cd62-470c-aafc-c3ce6b9' to ProfileId.
[
    "01458283-b6e3-4ae7-ae54-a68eb587cdc0",
    "927eb20f-cd62-470c-aafc-c3ce6b9",
    "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 is compatible.  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.7.1 36 1/1/2026
0.7.0 173 12/22/2025
0.6.0 203 11/9/2025
0.5.2 165 10/5/2025
0.5.1 170 9/25/2025
0.5.0 224 9/21/2025
0.4.0 151 9/13/2025
0.3.0 217 8/27/2025
0.2.7 84 8/23/2025
0.2.6 151 8/19/2025
0.2.5 127 8/17/2025
0.2.4 174 8/13/2025
0.2.3 165 8/8/2025
0.2.2 261 8/5/2025
0.2.1 211 8/4/2025
0.2.0 154 7/31/2025
0.1.9-alpha 160 7/27/2025
0.1.8-alpha 106 7/18/2025
0.1.7-alpha 159 7/9/2025
0.1.6-alpha 118 7/4/2025
0.1.5-alpha 122 6/20/2025
0.1.4-alpha 170 6/18/2025
0.1.3-alpha 138 5/30/2025
0.1.2-alpha 195 5/16/2025
0.1.1-alpha 187 5/16/2025
0.1.0-alpha 195 5/16/2025