FSharp.Data.YamlValue 0.1.1

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

FSharp.Data.YamlValue

A YAML document API for F#, in the shape of FSharp.Data's JsonValue.

Parse YAML into a discriminated union you can pattern match, navigate with the ? dynamic operator, read with AsInteger()-style accessors, and write back out — including comments.

Supported features

  • Flow and block YAML, arbitrarily nested — {...}/[...] flow collections and indentation-based block mappings/sequences, including compact notation (- key: value) and explicit ? key / : value pairs.
  • All YAML core-schema scalar types plus Timestamp — strings, integers (decimal/octal/hex), floats (including .inf/-.inf/.nan), booleans, null, and ISO-8601 dates/date-times — with the Norway problem (yes/no/on/off) deliberately left as strings.
  • Block and multi-line scalars| literal and > folded styles, with -/+ chomping and explicit indent indicators.
  • Anchors, aliases and merge keys (&anchor, *alias, <<: *anchor), resolved during parsing into a plain tree with shared (not copied) subtrees. Recursive anchors are a parse error.
  • Multi-document streams (---/...) and %YAML/%TAG directives, via ParseMultiple and YamlDocument.
  • Comment round-tripping through the separate YamlDocument type, which pairs a YamlValue with a path-keyed comment table — YamlValue.Parse itself stays comment-free.
  • Round-tripping emissionToString/WriteTo in block or flow style, with quoting rules that guarantee Parse(v.ToString(YamlSaveOptions.None)) = v (the property RoundTripTests checks via FsCheck).

Why

The existing .NET options solve a different problem. YamlDotNet and SharpYaml are C# object-mapping libraries; Legivel and YAMLicious are typed decoder libraries in the Thoth style. All of them want you to know your schema up front.

This library gives you the untyped document — the thing you reach for when you're exploring a config file, munging someone else's docker-compose.yml, or writing a tool that has to handle YAML it has never seen. Exactly what JsonValue does for JSON.

Install

dotnet add package FSharp.Data.YamlValue

Quick start

open FSharp.Data
open FSharp.Data.YamlExtensions

let info =
    YamlValue.Parse """
    name: Tomas          # given name
    born: 1985
    siblings:
      - Anna
      - Petr
    """

info?name.AsString()     // "Tomas"
info?born.AsInteger()    // 1985

for sibling in info?siblings do
    printfn "%s" (sibling.AsString())

Examples

Pattern matching

The whole point of the DU — you can take the document apart directly.

let rec describe value =
    match value with
    | YamlValue.String s    -> sprintf "string %s" s
    | YamlValue.Number n    -> sprintf "number %M" n
    | YamlValue.Float f     -> sprintf "float %f" f
    | YamlValue.Boolean b   -> sprintf "boolean %b" b
    | YamlValue.Timestamp t -> sprintf "timestamp %O" t
    | YamlValue.Null        -> "null"
    | YamlValue.Sequence xs -> xs |> Array.map describe |> String.concat ", "
    | YamlValue.Mapping ps  ->
        ps
        |> Array.map (fun (k, v) -> sprintf "%s = %s" (describe k) (describe v))
        |> String.concat "; "

Scalar types

Plain scalars are resolved using the YAML core schema. Quoted and block scalars are always strings, so you can always force a string by quoting it.

YamlValue.Parse "port: 8080"        // Mapping [| String "port", Number 8080M |]
YamlValue.Parse "port: '8080'"      // Mapping [| String "port", String "8080"  |]
YamlValue.Parse "debug: true"       // Boolean true
YamlValue.Parse "country: no"       // String "no"      <- not a boolean, see below
YamlValue.Parse "released: 2024-01-30"  // Timestamp 2024-01-30T00:00:00+00:00
YamlValue.Parse "missing: ~"        // Null

The core schema deliberately does not treat yes/no/on/off as booleans. This is the so-called Norway problem — under YAML 1.1, the country code NO silently parsed as false. Only true/false (and their capitalised forms) are booleans here.

Non-string keys

YAML allows any node as a mapping key, so Mapping holds (YamlValue * YamlValue)[]:

YamlValue.Parse "1: one"
// Mapping [| Number 1M, String "one" |]

YamlValue.Parse "? [a, b]\n: pair"
// Mapping [| Sequence [| String "a"; String "b" |], String "pair" |]

The ? operator, GetProperty and Properties all work on the string-keyed subset, so the common case stays as convenient as JsonValue. Use Entries when you need every key.

Anchors, aliases and merge keys

Anchors are resolved during parsing, so what you get back is a plain tree.

let compose =
    YamlValue.Parse """
    defaults: &defaults
      restart: always
      logging: json-file

    services:
      web:
        <<: *defaults
        image: nginx
      db:
        <<: *defaults
        image: postgres
    """

compose?services?web?restart.AsString()   // "always"  (merged from *defaults)
compose?services?web?image.AsString()     // "nginx"

Merge keys (<<:) are on by default. A recursive anchor is a parse error, since it cannot be represented as an immutable tree.

Multi-document streams

let docs = YamlValue.ParseMultiple """
kind: Service
name: web
---
kind: Deployment
name: web
"""

docs |> Seq.map (fun d -> d?kind.AsString())   // seq ["Service"; "Deployment"]

Loading

YamlValue.Load "config.yml"
YamlValue.Load "https://example.org/config.yml"
YamlValue.Load(File.OpenRead "config.yml")

async {
    let! config = YamlValue.AsyncLoad "https://example.org/config.yml"
    return config?version.AsString()
}

Writing YAML

let config =
    YamlValue.Mapping [|
        YamlValue.String "name",  YamlValue.String "web"
        YamlValue.String "port",  YamlValue.Number 8080M
        YamlValue.String "tags",  YamlValue.Sequence [| YamlValue.String "http"
                                                        YamlValue.String "public" |]
    |]

config.ToString(YamlSaveOptions.None)
// name: web
// port: 8080
// tags:
//   - http
//   - public

config.ToString(YamlSaveOptions.Flow)
// {name: web, port: 8080, tags: [http, public]}

A ToString() gotcha. F#/.NET type extensions can't override the virtual ToString() declared on the original type, so a genuinely bare x.ToString() call (no arguments at all) resolves to the compiler-generated structural formatter from [<StructuredFormatDisplay>] (handy in F# Interactive and debugger views: {name: "web"; port: 8080}), not the YAML emitter. Pass an argument — x.ToString(YamlSaveOptions.None) or x.ToString(2) — to reliably reach the real emitter. The same applies to YamlDocument.ToString().

The emitter quotes any string that would otherwise read back as a different type — "true", "8080", "null", "2024-01-30", the empty string — so Parse(v.ToString(YamlSaveOptions.None)) = v always holds.

Building and editing

YamlValue has no write API of its own — same as JsonValue, it's immutable. YamlBuilders adds an opt-in SetProperty/SetPath/RemoveProperty/RemovePath/GetPath/TryGetPath API that returns a new YamlValue rather than mutating in place, with a string-path DSL for nested reads and edits:

open FSharp.Data.YamlValueBuilders

let doc = YamlValue.Parse """
name: myapp
services:
  web:
    image: nginx
    ports: [80, 443]
"""

let updated =
    doc
        .SetProperty("name", YamlValue.String "myapp2")
        .SetPath("services.web.image", YamlValue.String "nginx:1.27")
        .RemovePath("services.web.ports[0]")

let image = updated.TryGetPath("services.web.image") // Some (YamlValue.String "nginx:1.27")

SetPath auto-vivifies missing intermediate mappings/sequences (mkdir -p-style) and pads a sequence with YamlValue.Null when the index is beyond its current length. RemovePath is a no-op when the path doesn't exist, and splices sequence elements out rather than leaving a Null hole. TryGetPath returns None (and GetPath raises) the moment any step of the path can't be followed.

The string-path DSL. Dot-separated segments address mapping keys, [n] addresses a sequence index, and a segment containing a literal ., [ or ] is bracket-quoted:

doc.SetPath("services.web.ports[0]", YamlValue.Number 8080M)
doc.GetPath("""config["a.b"]""")   // a literal '.' inside the key needs bracket-quoting

The DSL only expresses string-keyed mapping steps — non-string keys need the YamlPath fluent builder instead, via its Key(YamlValue) overload:

let path = YamlPath.Root.Key("services").Key("web").Index(0)
doc.SetPath(path, YamlValue.String "nginx:1.27")

See docs/reference.md for the full API, and examples/string-path-dsl.fsx for a runnable walkthrough of auto-vivification, bracket-quoting, overwriteScalars, and the YamlPath builder.

Comments

Comments are deliberately kept out of YamlValue so that pattern matching stays clean. When you need them, parse a YamlDocument instead — it pairs the value with a path-keyed comment table.

let doc = YamlDocument.Parse """
# Service configuration
name: web     # the public name
port: 8080
"""

doc.Value?name.AsString()                  // "web"
doc.ToString(YamlSaveOptions.None)         // round-trips, comments and all

YamlValue.Parse discards comments; YamlDocument.Parse preserves them.

How YAML maps onto YamlValue

YAML YamlValue
null, Null, NULL, ~, empty Null
true, True, TRUE, false, False, FALSE Boolean
123, -4, 0o17, 0xFF Number
1.5, 6.02e23 Number if exactly representable as decimal, else Float
.inf, -.inf, .nan Float
2024-01-30, 2024-01-30T09:15:00Z Timestamp
'quoted', "quoted", | and > blocks String
anything else plain String
key: value mappings, flow {} Mapping
- item sequences, flow [] Sequence

Explicit tags (!!str, !!int, !!bool, …) override resolution.

JsonValue parity

YamlValue is deliberately shaped like FSharp.Data.JsonValue — if you know one, you already mostly know the other. This table maps every JsonValue concept to its YamlValue equivalent, plus the YAML-only additions that don't have a JsonValue counterpart.

JsonValue YamlValue Note
Parse(text) Parse(text) Same name and shape. Comments are discarded; use YamlDocument.Parse to keep them.
TryParse(text) TryParse(text) Same name and shape.
ParseMultiple(text) YAML-only: splits a ----separated multi-document stream.
Load(stream \| reader \| uri) Load(stream \| reader \| uri) Same name and shape.
AsyncLoad(uri) AsyncLoad(uri) Same name and shape.
Request / RequestAsync Omitted; would require an HTTP dependency.
String of string String of string Same.
Number of decimal Number of decimal Same.
Float of float Float of float Same; also used for .inf/-.inf/.nan.
Boolean of bool Boolean of bool Same, but core-schema only — yes/no/on/off stay strings.
Null Null Same.
Record of (string * JsonValue)[] Mapping of (YamlValue * YamlValue)[] YAML permits non-string keys, so Mapping keys are YamlValue, not string.
Array of JsonValue[] Sequence of YamlValue[] YAML's term for the same shape.
Timestamp of DateTimeOffset YAML-only: a native scalar type for ISO-8601 dates/date-times.
( ? ) operator ( ? ) operator Same — value?propertyName, string-keyed only.
AsBoolean, AsInteger, AsInteger64, AsDecimal, AsFloat, AsString, AsDateTime, AsGuid Same names Identical signatures, including the ?cultureInfo parameter.
AsArray AsArray (alias AsSequence) AsSequence is a YAML-flavoured alias for the same accessor.
Properties Properties String-keyed pairs only, same as JsonValue.
Entries YAML-only: every mapping pair, including non-string keys.
GetProperty, TryGetProperty Same names Same signatures.
InnerText InnerText Same.
ToString(?saveOptions) ToString(saveOptions, ?indentationSpaces) / ToString(?indentationSpaces) Same idea; YAML adds an indentation parameter. See the ToString() gotcha above.
WriteTo(writer, saveOptions) WriteTo(writer, saveOptions, ?indentationSpaces) Same idea.
JsonSaveOptions YamlSaveOptions Adds Flow (JSON-style output) and ExplicitDocumentMarkers (---/...) to JsonValue's formatting flags.
YamlDocument YAML-only: pairs a YamlValue with comments, directives and multi-document support that JsonValue has no equivalent for.
Anchors / aliases (&anchor, *alias) YAML-only: resolved during parsing into a shared (not copied) immutable tree.
Merge keys (<<: *anchor) YAML-only: on by default.

Differences from JsonValue

The same information as above, condensed to the parts that actually differ:

JsonValue YamlValue Note
Record of (string * JsonValue)[] Mapping of (YamlValue * YamlValue)[] YAML permits non-string keys
Array Sequence YAML's term
Timestamp of DateTimeOffset native YAML scalar type
JsonSaveOptions YamlSaveOptions block vs. flow style, comment suppression
Request / RequestAsync omitted; would require an HTTP dependency
YamlDocument comments, directives, multi-document streams, anchors/aliases, merge keys

Everything else — Parse, TryParse, ParseMultiple, Load, AsyncLoad, WriteTo, the ? operator and the whole As* accessor family — matches JsonValue name for name.

Documentation

Full API reference: docs/reference.md.

Runnable, self-contained scripts for every feature above: examples/.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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 was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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.1.1 34 9/12/2026
0.1.0 43 9/11/2026

Replaced `sprintf`/`printf` usage across the parser, emitter, and path DSL with plain string concatenation. F#'s printf machinery builds format functions via reflection, which crashed under NativeAOT on any malformed YAML input (or on `ToString()`/`_Print`); the library is now verified to work in a NativeAOT-published app.
`IsAotCompatible` set on the library project.