F# JSON Type Provider: Compile-Time Safety Without the DTO Boilerplate
Stop hand-writing DTOs for every JSON payload. F#'s JSON Type Provider generates record types from a sample file at compile time, catching schema drift before runtime and giving you IntelliSense for free.
13 Feb 2026, 16:08 UTC

The Problem: Drifting JSON and Manual DTOs
Every backend developer knows the pain: an external API changes a field name, adds a new property, or makes a required field optional. Your hand-written DTOs fall out of sync, parsing fails at runtime, and you're left adding JsonPropertyName attributes or writing custom converters. In F#, the JSON Type Provider from FSharp.Data flips this model — it generates record types at compile time from a representative JSON sample, so the shape of your data is verified before the code even runs.
How It Works: Types from Samples
You point the provider at a JSON file (or inline literal) at compile time. It infers a hierarchy of F# records, using option for missing fields, arrays for collections, and nested records for objects. The generated types are erased — they exist only in F# source; at runtime they compile to plain .NET classes with no provider metadata. This means C# callers see ordinary objects, but F# callers get full IntelliSense and exhaustiveness checking.
Add the NuGet package FSharp.Data (v5+ for .NET 6+) to an F# project targeting .NET 8. Then reference a committed sample.json in your repo:
// Sample.json (checked in)
{
"userId": 42,
"username": "alice",
"roles": ["admin", "editor"],
"profile": {
"email": "[contact removed]",
"lastLogin": "2026-10-15T08:30:00Z"
}
}
open FSharp.Data
// Compile-time type generation
type User = JsonProvider<"sample.json">
// Parsing at runtime
let parseUser (json: string) =
User.Parse(json)
IntelliSense now shows UserId, Username, Roles (string array), and Profile (nested record with Email and LastLogin). If the live payload omits lastLogin, the generated Profile.LastLogin is option<DateTime> — no null reference exceptions.
Worked Example: Handling Schema Drift in CI
A practical pattern: keep sample.json as the contract, and add a CI step that validates live responses against the same generated types. Here's a minimal script that fetches a test endpoint and parses it with the provider types:
// validate.fsx (run in CI)
#r "nuget: FSharp.Data"
open FSharp.Data
open System.Net.Http
// Same provider as production code
type User = JsonProvider<"sample.json">
let http = HttpClient()
let! response = http.GetStringAsync "https://api.example.com/users/42" |> Async.AwaitTask
try
let user = User.Parse(response)
printfn "OK: %s (%s)" user.Username (string user.UserId)
0
with
| :? JsonReaderException as ex ->
eprintfn "Schema mismatch: %s" ex.Message
1
| ex ->
eprintfn "Unexpected error: %s" ex.Message
1
Run this in a pipeline step. If the API adds a new field, the provider ignores it (forward compatibility). If a required field like userId disappears, User.Parse throws a JsonReaderException with a clear message — the build fails, forcing you to update sample.json and decide whether the field should become optional.
Trade-offs and Limitations
- Compile-time dependency: The sample file must be accessible to the build agent. Remote URLs are not fetched during compilation; commit a representative sample or embed it as a resource.
- Erased types: From C#, the generated types appear as plain classes (often
JObject-like). If you expose these types across a public API boundary, define explicit DTOs in a shared library for stability. - No JSON Schema validation: The provider checks structural shape only — no
minLength,pattern, or custom format constraints. Add runtime validation (e.g., withSystem.ComponentModel.DataAnnotations) if you need those guarantees. - Compilation cost: Large or deeply nested samples slow down the type provider. Split into multiple smaller providers or use a minimal representative subset via the
Sampleparameter.
Performance Note
For high-throughput paths, the provider can emit parsing code that uses System.Text.Json directly, avoiding reflection. The generated Parse method returns a strongly-typed result with helpful error messages. Benchmark your payload size with BenchmarkDotNet against manual JsonSerializer.Deserialize to confirm the overhead is acceptable — typically within 5-10% for medium-sized objects.
Actionable Next Step
Create a new F# console project, add FSharp.Data, drop a real API response into sample.json, and write type T = JsonProvider<"sample.json">. Rebuild after adding a field to the sample — watch IntelliSense update instantly. Then wire the CI validation script. You'll catch breaking changes at build time, not in production logs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.