YAML serialisation, deserialisation and json schema generation utility, with support for primary constructors and same-assembly inheritance.
$ dotnet add package YamlRecordsA small script that can deserialize and serialize to YAML from dotnet classes; it supports C# 9 records with primary constructors, and can also figure out inheritance with some derived type heuristics.
I built this for a Godot game I was working on, where I wanted to use minimal Record type definitions and inheritance. The incumbent dotnet project for YAML is YamlDotNet but at the time of writing (September 2025) that project, while being vastly more sophisticated and tested than this humble script, had not adapted to Records with primary constructors yet (there are workarounds that include adding parameterless constructors to each record, a bit ugly). Additionally it didn't support inheritance very well when deserializing, a perennial issue with serializers. I needed both.
To use, either use the Nuget package (link above), or just copy YamlRecords.cs into your project somewhere (change namespaces or trim down as needed) - its been built to just use the standard library, no external dependencies.
Note: I built this for my needs, and its possible it won't cover all edge cases - I've tried to make it fairly generic for things like lists and collections, nullable types and enums etc, but don't expect it to be perfect.
Note: All of the below steps are performed in YamlRecords.Tests.cs; the only file you need for your own projects is YamlRecords.cs - the Nuget package also excludes all this testing stuff and their dependencies.
Using the record types defined in YamlRecords.Tests.cs, you can define a structure like this:
var test = new GameConfig(new()
{
{ "funds", new CardType("Funds", "res://assets/wealth_icon.png") },
{ "health", new CardType("Health", "res://assets/reputation_icon.png") },
},
new()
{
{ "work", new GameFlow("res://assets/authority_icon.png", "choose_path", new() {
{
"choose_path", new SocketState(new ()
{
{"default", new StateVariant("Work", "Choose your path to earn funds", "Start", null)},
{"labour", new StateVariant("Work", "Physical work for small pay", "Start", new TransitionAction("labour"))}
}
, "default", [
new SocketConfig("work", ["reason","health","passion"], new() {
{ "health", new VariantAction("labour") }
})
])
},
{
"labour", new TimerState(new ()
{
{"default", new StateVariant("Work", "The day stretches long, your hand's burn", "Running...", null)},
}
, "default", 60, null, new TransitionAction("choose_path")) // repeat for test
},
}) }
});
Note that this structure includes abstract types and their derived types, as well as record types using exclusively primary constructors.
You can then serialize this with:
var yaml = YamlRecords.Serialize(test);
Console.WriteLine(yaml);
Note the namespace is
YamlRecordsSerializer
Which will produce:
cardTypes:
funds:
title: Funds
iconPath: "res://assets/wealth_icon.png"
health:
title: Health
iconPath: "res://assets/reputation_icon.png"
gameFlows:
work:
iconPath: "res://assets/authority_icon.png"
startState: choose_path
states:
choose_path:
sockets:
- title: work
accepts:
- reason
- health
- passion
onAccept:
health:
newVariant: labour
variants:
default:
title: Work
description: Choose your path to earn funds
actionLabel: Start
onAction:
labour:
title: Work
description: Physical work for small pay
actionLabel: Start
onAction:
newState: labour
defaultVariant: default
labour:
seconds: 60
socket:
onElapsed:
newState: choose_path
variants:
default:
title: Work
description: "The day stretches long, your hand's burn"
actionLabel: Running...
onAction:
defaultVariant: default
Which you can then deserialize with:
var test = YamlRecords.Deserialize<GameConfig>(yaml);
And have no issues (re-serialize if necessary, to prove that the populated classes will once again generate identical YAML).
The tool can also generate a json schema for a type, complete with support for derived types. For example:
var schema = YamlRecords.GenerateSchema<GameConfig>()
will generate something like:
{
"type": "object",
"title": "gameConfig",
"nullable": true,
"properties": {
"cardTypes": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/CardType"
}
},
"gameFlows": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/GameFlow"
}
},
"startingCards": {
"type": "array",
"items": {
"type": "string"
}
},
"startingFlows": {
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"defs": {
"CardType": {
"type": "object",
"title": "cardType",
"nullable": true,
"properties": {
"title": {
"type": "string"
},
"iconPath": {
"type": "string"
}
},
"additionalProperties": false
},
"GameFlow": {
"type": "object",
"title": "gameFlow",
"nullable": true,
"properties": {
"iconPath": {
"type": "string"
},
"startState": {
"type": "string"
},
"states": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/FlowState"
}
}
},
"additionalProperties": false
},
"FlowState": {
"type": "object",
"title": "flowState",
"nullable": true,
"properties": {
"variants": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/StateVariant"
}
},
"defaultVariant": {
"type": "string"
}
},
"oneOf": [
{
"$ref": "#/defs/SocketState"
},
{
"$ref": "#/defs/TimerState"
},
{
"$ref": "#/defs/CardState"
}
]
},
"StateVariant": {
"type": "object",
"title": "stateVariant",
"nullable": true,
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"actionLabel": {
"type": "string"
},
"onAction": {
"$ref": "#/defs/StateAction"
}
},
"additionalProperties": false
},
"SocketState": {
"type": "object",
"title": "socketState",
"nullable": true,
"properties": {
"sockets": {
"type": "array",
"items": {
"$ref": "#/defs/SocketConfig"
}
},
"variants": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/StateVariant"
}
},
"defaultVariant": {
"type": "string"
}
},
"additionalProperties": false
},
"TimerState": {
"type": "object",
"title": "timerState",
"nullable": true,
"properties": {
"seconds": {
"type": "number"
},
"socket": {
"$ref": "#/defs/SocketConfig"
},
"onElapsed": {
"$ref": "#/defs/StateAction"
},
"variants": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/StateVariant"
}
},
"defaultVariant": {
"type": "string"
}
},
"additionalProperties": false
},
"CardState": {
"type": "object",
"title": "cardState",
"nullable": true,
"properties": {
"newCards": {
"type": "array",
"items": {
"type": "string"
}
},
"variants": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/StateVariant"
}
},
"defaultVariant": {
"type": "string"
}
},
"additionalProperties": false
},
"StateAction": {
"type": "object",
"title": "stateAction",
"nullable": true,
"oneOf": [
{
"$ref": "#/defs/TransitionAction"
},
{
"$ref": "#/defs/VariantAction"
}
]
},
"SocketConfig": {
"type": "object",
"title": "socketConfig",
"nullable": true,
"properties": {
"title": {
"type": "string"
},
"accepts": {
"type": "array",
"items": {
"type": "string"
}
},
"onAccept": {
"type": "object",
"additionalProperties": {
"$ref": "#/defs/StateAction"
}
}
},
"additionalProperties": false
},
"TransitionAction": {
"type": "object",
"title": "transitionAction",
"nullable": true,
"properties": {
"newState": {
"type": "string"
}
},
"additionalProperties": false
},
"VariantAction": {
"type": "object",
"title": "variantAction",
"nullable": true,
"properties": {
"newVariant": {
"type": "string"
}
},
"additionalProperties": false
}
}
}