A data transfer object (DTO) carries data between software components without domain behavior. In F#, simple data types commonly remain separate from functions that operate on them. section 5.1.1
A practical DTO mirrors an external format closely, especially JSON. Keeping this intermediary model simple reduces deserialization problems before validated data enters the rich domain model. section 10.2.1
A useful DTO boundary has three properties:
- Uses uncomplicated fields that match the incoming representation, such as strings and string lists. section 10.2.1
- Avoids domain rules and validation inside serialization converters. section 10.2.1
- Maps raw values into domain types through ordinary F# functions that can return validation errors. section 10.2.1
Guitar inventory import
Define a RawGuitar record with Brand: string, Strings: string, and Pickups: string list. Deserialize the JSON into RawGuitar, then pipe it to tryAsFullGuitar, which validates the fields and constructs the richer Guitar domain value. section 10.2.1
Listing 10.3 Using raw types to simplify JSON serialization
#r "nuget:FsToolkit.ErrorHandling"
open FsToolkit.ErrorHandling #1
type RawGuitar = #2
{
Brand: string
Strings: string
Pickups: string list
}
let tryAsFullGuitar (raw:RawGuitar) = result { #3
let! brand = #4
if String.IsNullOrWhiteSpace raw.Brand then
Error "Brand is mandatory"
else
Ok (Brand raw.Brand)
let! strings = #4
match raw.Strings with
| "6" -> Ok Six
| "7" -> Ok Seven
| "8" -> Ok Eight
| "12" -> Ok Twelve
| value -> Error $"Invalid value '{value}'"
let! pickups = #4
raw.Pickups
|> List.traverseResultM (fun pickup -> #5
match pickup with
| "S" -> Ok Single
| "H" -> Ok Humbucker
| value -> Error $"Invalid value {value}")
return
{
Guitar.Brand = brand
Guitar.Strings = strings
Guitar.Kind =
match pickups with
| [] -> Acoustic #6
| pickups -> Electric pickups
}
}
Inspect the complete raw record and mapping pattern, including validation of mandatory brands, string counts, and pickup data. section 10.2.1





