- Notifications
You must be signed in to change notification settings - Fork30
fsprojects/Fleece
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Fleece is a JSON mapper for F#. It simplifies mapping from a Json library's JsonValue onto your types, and mapping from your types onto JsonValue.
The Json library could beSystem.Json,System.Text.Json,FSharp.Data's orNewtonSoft's Json.NET.
Its design is strongly influenced by Haskell'sAeson. Like Aeson, Fleece is designed around two typeclasses (inFSharpPlus style) ToJson and OfJson.
For example, given this data type:
typePerson={ Name:string Age:int Children:Person list}
You can map it to JSON like this:
openFleeceopenFleece.OperatorstypePersonwithstatic memberToJson(x:Person)= jobj["name".= x.Name"age".= x.Age"children".= x.Children]letp={ Person.Name="John" Age=44 Children=[{ Person.Name="Katy" Age=5 Children=[]}{ Person.Name="Johnny" Age=7 Children=[]}]}// Test with System.Text.JsonopenFleece.SystemTextJsonprintfn"%s"(toJsonText p)
And you can map it from JSON like this:
typePersonwithstatic memberOfJson json=match jsonwith| JObject o->letname= o.@"name"letage= o.@"age"letchildren= o.@"children"match name, age, childrenwith| Decode.Success name, Decode.Success age, Decode.Success children-> Decode.Success{ Person.Name= name Age= age Children= children}| x-> Error<| Uncategorized(sprintf"Error parsing person:%A" x)| x-> Decode.Fail.objExpected xletjohn:Person ParseResult= ofJsonText"""{"name": "John", "age": 44, "children": [{"name": "Katy", "age": 5, "children": []}, {"name": "Johnny", "age": 7, "children": []}]}"""
Though it's much easier to do this in a monadic or applicative way. For example, usingFSharpPlus (which is already a dependency of Fleece):
openFSharpPlustypePersonwithstatic memberCreate name age children={ Person.Name= name; Age= age; Children= children}static memberOfJson json=match jsonwith| JObject o-> Person.Create<!>(o.@"name")<*>(o.@"age")<*>(o.@"children")| x-> Decode.Fail.objExpected x
or with applicatives:
openFSharpPlustypePersonwithstatic memberOfJson json=match jsonwith| JObject o-> monad{let!name= o.@"name"and! age= o.@"age"and! children= o.@"children"return{ Person.Name= name Age= age Children= children}}| x-> Decode.Fail.objExpected x
Or you can use the Choice monad/applicative inFSharpx.Extras instead, if you prefer.
You can see more examples in theEdmundsNet project.
For types that deserialize to Json Objets, typically (but not limited to) records, you can alternatively use codecs and have a single method which maps between fields and values.
typePerson={ name:string* string age:int option children:Person list}withstatic memberget_Codec()=fun f l a c->{ name=(f, l); age= a; children= c}<!> jreq"firstName"(Some<<fun x-> fst x.name)<*> jreq"lastName"(Some<<fun x-> snd x.name)<*> jopt"age"(fun x-> x.age)// Optional fields can use 'jopt'<*> jreq"children"(fun x-> Some x.children)|> ofObjCodecletjohn:Person ParseResult= ofJsonText"""{"name": "John", "age": 44, "children": [{"name": "Katy", "age": 5, "children": []}, {"name": "Johnny", "age": 7, "children": []}]}"""
Discriminated unions can be modeled with alternatives:
typeShape=| Rectangleofwidth:float*length:float| Circleofradius:float| Prismofwidth:float*float*height:floatwithstatic memberget_Codec()=(Rectangle<!> jreq"rectangle"(function Rectangle(x, y)-> Some(x, y)|_-> None))<|>(Circle<!> jreq"radius"(function Circle x-> Some x|_-> None))<|>(Prism<!> jreq"prism"(function Prism(x, y, z)-> Some(x, y, z)|_-> None))|> ofObjCodec
or using the jchoice combinator:
typeShapewithstatic memberJsonObjCodec= jchoice[ Rectangle<!> jreq"rectangle"(function Rectangle(x, y)-> Some(x, y)|_-> None) Circle<!> jreq"radius"(function Circle x-> Some x|_-> None) Prism<!> jreq"prism"(function Prism(x, y, z)-> Some(x, y, z)|_-> None)]|> ofObjCodec
But codecs for both types can easily be written with the codec computation expressions
typePerson={ name:string* string age:int option children:Person list}withstatic memberget_Codec()= codec{let!f= jreq"firstName"(Some<<fun x-> fst x.name)and! l= jreq"lastName"(Some<<fun x-> snd x.name)and! a= jopt"age"(fun x-> x.age)// Optional fields can use 'jopt'and! c= jreq"children"(fun x-> Some x.children)return{ name=(f, l); age= a; children= c}}|> ofObjCodectypeShape=| Rectangleofwidth:float*length:float| Circleofradius:float| Prismofwidth:float*float*height:floatwithstatic memberget_Codec()= codec{ Rectangle<!> jreq"rectangle"(function Rectangle(x, y)-> Some(x, y)|_-> None) Circle<!> jreq"radius"(function Circle x-> Some x|_-> None) Prism<!> jreq"prism"(function Prism(x, y, z)-> Some(x, y, z)|_-> None)}|> ofObjCodec
What's happening here is that we're getting a Codec to/from a Json Object (not neccesarily a JsonValue) which Fleece is able to take it and fill the gap by composing it with a codec from JsonObject to/from JsonValue.
We can also do that by hand, we can manipulate codecs by using functions in the Codec module. Here's an example:
openSystem.TextopenFleece.SystemTextJson.OperatorstypePerson={ name:string* string age:int option children:Person list}withstatic memberJsonObjCodec:Codec<PropertyList<Fleece.SystemTextJson.Encoding>,Person>= codec{let!f= jreq"firstName"(Some<<fun x-> fst x.name)and! l= jreq"lastName"(Some<<fun x-> snd x.name)and! a= jopt"age"(fun x-> x.age)// Optional fields can use 'jopt'and! c= jreq"children"(fun x-> Some x.children)return{ name=(f, l); age= a; children= c}}letpersonBytesCodec= Person.JsonObjCodec|> Codec.compose jsonObjToValueCodec// this is the codec that fills the gap to/from JsonValue|> Codec.compose jsonValueToTextCodec// this is a codec between JsonValue and JsonText|> Codec.invmap(Encoding.UTF8.GetString: byte[]-> string) Encoding.UTF8.GetBytes// This is a pair of of isomorphic functionsletp={ name="John","Smith"; age= Some42; children=[]}letbytePerson= Codec.encode personBytesCodec p// val bytePerson : byte [] = [|123uy; 34uy; 102uy; 105uy; 114uy; 115uy; ... |]letp'= Codec.decode personBytesCodec bytePerson
So far we've seen how Fleece is capable of encoding/decoding by deriving automatically a codec from static members in the type.
But for those cases where we don't have control over the types (extension members won't be taken into account) we can explicitly specify combinators.
To do so, a set of the available functions exists, ending with theWith
suffix, which accepts a combinator as first parameter:
typeColor= Red| Blue| WhitetypeCar={ Id:string Color:Color Kms:int}letcolorDecoder=function| JString"red"-> Decode.Success Red| JString"blue"-> Decode.Success Blue| JString"white"-> Decode.Success White| JString xas v-> Decode.Fail.invalidValue v("Wrong color:"+ x)| x-> Decode.Fail.strExpected xletcolorEncoder=function| Red-> JString"red"| Blue-> JString"blue"| White-> JString"white"letcolorCodec()= colorDecoder<-> colorEncoderletcarCodec()= codec{let!i= jreqWith Codecs.string"id"(fun x-> Some x.Id)and! c= jreqWith colorCodec"color"(fun x-> Some x.Color)and! k= jreqWith Codecs.int"kms"(fun x-> Some x.Kms)return{ Id= i; Color= c; Kms= k}}|> Codec.compose(Codecs.propList Codecs.id)letcar={ Id="xyz"; Color= Red; Kms=0}letjsonCar:Fleece.SystemTextJson.Encoding= Codec.encode(carCodec()) car// val jsonCar: SystemTextJson.Encoding = {"id":"xyz","color":"red","kms":0}
Json lenses allow to focus on a specific part of the json structure to perform operations like view, write and update.
For a quick reference have a look atthis test file