Architectural Design for F# Type Providers in Production
F# Type Providers let you generate strongly‑typed models from live JSON, CSV, or SQL schemas at compile time. This article explains the architectural trade‑offs, how to treat schema sources as trusted code, CI checks to guard against drift, and when to shift to runtime validation.
15 Jan 2026, 22:54 UTC

Problem & Takeaway
When a .NET service consumes external data (JSON APIs, CSV feeds, or relational tables), developers often hand‑craft DTOs and validation layers. F# Type Providers let you generate strongly‑typed representations from live schema sources at compile time, eliminating the boilerplate. The key architectural decision is how to treat the provider as part of the trusted computing base, manage schema drift, and verify the generated types in CI, while keeping the system robust against runtime data anomalies.
Requirements
- Compile‑time access to the schema source (JSON sample, CSV header, or live DB).
- Versioned, reproducible provider libraries (e.g.,
FSharp.Data,SQLProvider). - CI pipeline that can run
dotnet buildanddotnet fsiwith the same environment as production. - Mechanism to capture and compare the generated type snapshot.
Smallest Suitable Design
Generate one provider type per external schema and consume it directly in domain code. Avoid DTO mapping layers; the provider type becomes the domain model. For example:
#r "nuget: FSharp.Data, 2.5.0"
open FSharp.Data
// JSON provider from a sample file
let json = JsonProvider<
"/path/to/sample.json",
SampleIsList = true
>()
let record = json.[0]
let price = record.Price
For SQL, generate a module with table types:
#r "nuget: SQLProvider, 1.8.0"
open SQLProvider
let db = SqlDataConnection<
ProviderName = "Microsoft.Data.SqlClient",
ConnectionString = "Server=.;Database=Demo;Trusted_Connection=True;",
ResolutionPath = __SOURCE_DIRECTORY__
>
let users = db.GetTable<Users>()
These generated types are structural; they carry only the shape, not business invariants.
Trust & Data Boundaries
Because the provider runs arbitrary code during compilation, the schema source becomes part of the trusted computing base. Treat the sample file or database connection string as code artifacts under version control. If a malicious actor swaps sample.json with a payload that introduces a new field, the compiler will generate a new type that compiles but may break runtime logic if not handled.
Data boundary: the provider erases to .NET base types (e.g., obj, string, int). No runtime reflection over the shape is possible. Domain logic must still enforce business rules via smart constructors or refinement types. For instance, a price field should be validated to be non‑negative after deserialization.
Operational Checks & CI
- Schema snapshot – Commit a hash of the generated type names to the repository. Example script:
dotnet fsi --eval \ "#r \"nuget: FSharp.Data\"; open FSharp.Data; \ let t = JsonProvider<\"sample.json\">; \ let types = typeof<t> |> Type.GetMembers |> Array.filter (fun m -> m.MemberType = System.Reflection.MemberTypes.NestedType) \ |> Array.map (fun m -> m.Name) |> Array.sort |> String.concat \ let hash = System.Security.Cryptography.SHA256.Create().ComputeHash(System.Text.Encoding.UTF8.GetBytes(types)) |> BitConverter.ToString |> String.replace "-" "" \ printfn "%s" hash" | tee snapshot.sha - CI build step – Run
dotnet buildand compare the new snapshot to the committed one. Fail if they differ, indicating schema drift. - Runtime guard – Wrap deserialization in try/catch and log any
JsonExceptionorCsvException. UseJsonSerializerOptionswithAllowTrailingCommas = falseto enforce strictness. - Database connectivity – For SQLProvider, use the
SchemaOnlymode or a Dockerised test database in CI to avoid relying on a shared prod instance.
Failure Modes
- Offline build – If the schema source is unreachable (e.g., no network in CI), the provider fails. Mitigate by checking in a static snapshot of the schema (JSON sample, CSV header file, or SQL schema script).
- Provider version drift – Different versions of
FSharp.DataorSQLProvidermay change type inference rules. Pin the package version inpaket.dependenciesor*.fsprojand enforce it in CI. - Runtime data violations – Data may contain nulls in non‑nullable columns, or extra JSON properties that the provider ignores. Detect these at the deserialization boundary and reject or transform the payload.
- Security risk – The provider may execute arbitrary code at compile time (e.g., downloading a schema). Restrict network access in build agents and audit the provider implementation.
When to Change the Design
- Fast schema churn – If external schemas change more often than deployment cycles, consider moving to runtime validation (e.g.,
System.Text.Jsonwith custom converters) and accept duplicate DTOs. - Cross‑language consumers – When other services need the same schema, generate the types from a shared contract language such as OpenAPI or Protobuf instead of reverse‑engineering.
- Cold‑start latency – If compile time is critical, pre‑generate the provider .fs files during CI and commit them. The build then uses the static file instead of fetching the schema.
Conclusion
Using F# Type Providers as a compile‑time integration mechanism can dramatically reduce boilerplate and enforce shape correctness, but it also shifts the trust boundary to the provider and the schema source. By treating schema artifacts as code, pinning provider versions, capturing snapshots, and adding CI checks, you can detect drift before it reaches production. Still, be vigilant for runtime anomalies and be ready to switch to a more tolerant validation strategy if the schema evolves too rapidly or if cross‑language compatibility is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.