fsharp-serialization
Skill23 starsChanged 32 days agoMITView on GitHub
Serialize F# records, unions, options, and maps to JSON that matches a wire contract, and keep old JSON readable. Use when writing or reviewing F# code that calls System.Text.Json, configures FSharp.SystemTextJson (JsonFSharpConverter, JsonFSharpOptions), writes Thoth.Json encoders or decoders, adds a DTO layer, reads JSON from another service, or adds or renames a union case or record field that is stored or sent as JSON.
In this repository16 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- fsharp-async-compositionSkill
- fsharp-binary-compatibilitySkill
- fsharp-env-capabilitiesSkill
- fsharp-failure-pathsSkill
- fsharp-idiom-reviewSkill
- fsharp-iwsam-errorsSkill
- fsharp-library-api-designSkill
- fsharp-native-aotSkill
- fsharp-nullness-interopSkill
- fsharp-performance-typesSkill
- fsharp-signature-filesSkill
- fsharp-verify-apiSkill
- optimize-fsharp-typecheck-graphSkill
- parse-dont-validateSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 5 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈5
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 15
Write the wire contract before you choose settings.
- Very specific: line 16
Do not use System.Text.Json alone for F# domain types.
- Very specific: line 17
Do not keep the FSharp.SystemTextJson defaults for a contract that you do not own.
- Very specific: line 22
Do not use DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull for this.
- Distinctive: line 25
When the wire shape differs from the domain types, or the domain types have private constructors, decode into a DTO or with a Thoth.Json …
- Distinctive: line 28
Read references/SETTINGS.md for the System.Text.Json defaults, each FSharp.SystemTextJson union encoding with its output, and the option, …
- Distinctive: line 29
Read references/WIRE-COMPATIBILITY.md before you add, rename, or remove a union case or record field that is stored or sent, and for the …
- Distinctive: line 33
Write the wire contract as a list or table.
- Very specific: line 38
Run the cases. Confirm that no output has a Case or Fields property unless the contract uses it, and that the decode function returns Error …
- Distinctive: line 42
Run the proof script from the skill root:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
F# serialization
System.Text.Json was made for C# classes. With F# types it throws, writes a shape that other services cannot read, or reads missing data as null or 0 with no error.
FSharp.SystemTextJson fixes the exceptions, but its defaults write a shape that other services do not use.
A serializer that round-trips its own output can still disagree with the other service on every message.
Rules
- Write the wire contract before you choose settings. For each type, record the property names, the union shape, what an absent or
nulloptional value means, the map key shape, the required fields, and what to do with unknown properties and unknown cases. - Do not use System.Text.Json alone for F# domain types. It throws
NotSupportedExceptionon a union. It fills a missing record field withnullor0, and it reads the JSONnullas anullrecord. - Do not keep the FSharp.SystemTextJson defaults for a contract that you do not own. The defaults write
{"Case":...,"Fields":[...]}, reject a missing option field, and writeMap<int, _>as an array of pairs. - Set each union encoding, the union tag name, and the union tag naming policy.
PropertyNamingPolicydoes not change union tags. - Encoding flags apply to every union. When one union needs a different shape, use
WithOverrides. A[<JsonFSharpConverter>]attribute on a type has no effect when global options exist, unless you callWithAllowOverride. - To leave out
Noneand read an absent ornullvalue asNone, useWithSkippableOptionFields(SkippableOptionFields.Always, deserializeNullAsNone = true).WithSkippableOptionFields()readsnullin astring optionasSome null. Do not useDefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNullfor this. It reads a missing requiredstring,Map, or union case field asnulland throwsNullReferenceExceptionfor a missingint. - A single-case union around an option (
Box of int option) writesnullforBox Noneand reads it back as anullunion. An option of an option writesSome Noneasnull. Change the type or write a converter. - At the decode boundary, return
Errorfor aJsonException. When the code reads with System.Text.Json alone, into a DTO class, or into a single-case union around an option, also returnErrorfor anullresult. Run the cases that other exceptions can come from, and fix the settings that cause them. - When the wire shape differs from the domain types, or the domain types have private constructors, decode into a DTO or with a Thoth.Json decoder, then parse into the domain type. See parse-dont-validate.
- For Native AOT and trimming, use fsharp-native-aot. This skill does not cover source generation.
Read references/SETTINGS.md for the System.Text.Json defaults, each FSharp.SystemTextJson union encoding with its output, and the option, map, and naming settings. Read references/WIRE-COMPATIBILITY.md before you add, rename, or remove a union case or record field that is stored or sent, and for the saved-sample tests, Thoth.Json, and DTO layers.
Steps
- Write the wire contract as a list or table. Done when each union, option, map, and required field has a rule.
- Get JSON samples that the other side wrote, or that the current code wrote before the change. Save them as files next to the tests.
- Choose FSharp.SystemTextJson settings, Thoth.Json coders, or a DTO layer. Make one options value for each wire contract, and use it for every call on that contract.
- For each saved sample, check that it decodes to the expected F# value, and that the expected value encodes to the same JSON. Compare parsed JSON, not text, so that property order and white space do not count.
- For each contract rule that no sample shows, add a case: an absent optional value, a
nulloptional value, a missing required field, anullrequired field, an unknown property, an unknown union case, the JSONnull, and malformed JSON. - Run the cases. Confirm that no output has a
CaseorFieldsproperty unless the contract uses it, and that the decode function returnsErrorand does not throw.
Verify
Run the proof script from the skill root:
pwsh ./scripts/run.ps1The script shows each default and setting in this skill with FSharp.SystemTextJson 1.4.36 and Thoth.Json.System.Text.Json 0.5.0 on .NET 10. It fails when a result differs from the expected result.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

