WebSharper provides a convenient and readable JSON serialization format for F# types as well as C# classes. The structure of the JSON is inferred from the type, and can be customized using attributes. This format is usable both from the server and the client side.
WebSharper Sitelets provide facilities to both parse JSON from HTTP requests and write it to HTTP responses.
- Parsing: using the
[<Json>]
attribute. - Writing: using Content.Json.
The WebSharper.Json
module provides the following functions:
Serialize : 'T -> string
serializes a value to string.Deserialize : string -> 'T
deserializes a value from a string.
JSON serialization is also available on the client.
Two modules both named WebSharper.Json
provide the following functions:
Parse : string -> obj
uses JavaScript'sJSON.parse
to convert a string to a value (no attribute-based transformations).Stringify :obj -> string
uses JavaScript'sJSON.stringify
to convert a value to a string (no attribute-based transformations).Encode : 'T -> obj
converts a value to a JavaScript object, such thatJson.Stringify (Json.Encode x) = Json.Serialize x
.Decode : obj -> 'T
converts a JavaScript object to a value, such thatJson.Decode (Json.Parse s) = Json.Deserialize s
.
The following base types are handled:
- Integers:
int8
,int16
,int32
(akaint
),int64
Content.Json 12y
// Output: 12
- Unsigned integers:
uint8
(akabyte
),uint16
,uint32
,uint64
Content.Json 12ul
// Output: 12
- Floats:
single
,double
(akafloat
)
Content.Json 12.34
// Output: 12.34
- Decimals:
decimal
Content.Json 12.34m
// Output: 12.34
- Strings:
string
Content.Json """A string with some "content" inside"""
// Output: "A string with some \"content\" inside"
- Booleans:
bool
Content.Json true
// Output: true
Values of type list<'T>
, 'T[]
and Set<'T>
are represented as JSON arrays:
Content.Json [|"a string"; "another string"|]
// Output: ["a string", "another string"]
Content.Json (Set ["a string"; "another string"])
// Output: ["another string", "a string"]
Values of type Map<string, 'T>
and System.Collections.Generic.Dictionary<string, 'T>
are represented as flat JSON objects:
Content.Json (Map [("somekey", 12); ("otherkey", 34)])
// Output: {"somekey": 12, "otherkey": 34}
Other Map
and Dictionary
values are represented as an array of key-value pairs:
Content.Json (Map [(1, 12); (3, 34)])
// Output: [[1, 12], [3, 34]]
Tuples (including struct tuples) are also represented as JSON arrays:
Content.Json ("a string", "another string")
// Output: ["a string", "another string"]
Content.Json (struct ("a string", "another string")
// Output: ["another string", "a string"]
F# records are represented as flat JSON objects. The attribute [<Name "name">]
can be used to customize the field name:
type Name =
{
[<Name "first-name">] FirstName: string
LastName: string
}
type User =
{
name: Name
age: int
}
Content.Json {name = {FirstName = "John"; LastName = "Doe"}; age = 42}
// Output: {"name": {"first-name": "John", "LastName": "Doe"}, "age": 42}
Union types intended for use in JSON serialization should optimally bear the attribute NamedUnionCases
for producing fully readable JSON format.
There are two ways to use it, specifying a field name to hold the union case name or signaling that the case should be inferred from the field names.
If no NamedUnionCases
is present, a "$"
field will be used for storing the case index.
With [<NamedUnionCases "field">]
, the union value is represented as a JSON object with a field called "field"
, whose value is the name of the union case, and as many other fields as the union case has arguments. You can use [<Name "name">]
to customize the name of a union case.
[<NamedUnionCases "kind">]
type Contact =
| [<Name "address">]
Address of street: string * zip: string * city: string
| Email of email: string
Content.Json
[
Address("12 Random St.", "15243", "Unknownville")
Email "[email protected]"
]
// Output: [
// {"kind": "address",
// "street": "12 Random St.",
// "zip": "15243",
// "city": "Unknownville"},
// {"kind": "Email",
// "email": "[email protected]"}
// ]
Unnamed fields receive the names Item1
, Item2
, etc.
Missing the attribute, the case name would be not stored in a readable form:
type Contact =
| Address of street: string * zip: string * city: string
| Email of email: string
Content.Json
[
Address("12 Random St.", "15243", "Unknownville")
Email "[email protected]"
]
// Output: [
// {"$": 0,
// "street": "12 Random St.",
// "zip": "15243",
// "city": "Unknownville"},
// {"$": 1,
// "email": "[email protected]"}
// ]
With an argumentless [<NamedUnionCases>]
, no extra field is added to determine the union case; instead, it is inferred from the names of the fields present. This means that each case must have at least one mandatory field that no other case in the same type has, or a compile-time error will be thrown.
[<NamedUnionCases>]
type Contact =
| Address of street: string * zip: string * city: string
| Email of email: string
Content.Json
[
Address("12 Random St.", "15243", "Unknownville")
Email "[email protected]"
]
// Output: [
// {"street": "12 Random St.",
// "zip": "15243",
// "city": "Unknownville"},
// {"email": "[email protected]"}
// ]
As a special case, if a union case has a single, unnamed argument which is a record, then the fields of this record are used as the fields of the output object.
type Address = { street: string; zip: string; city: string }
[<NamedUnionCases>]
type Contact =
| Address of Address
| Email of email: string
Content.Json
[
Address {
street = "12 Random St."
zip = "15243"
city = "Unknownville"
}
Email "[email protected]"
]
// Output: [
// {"street": "12 Random St.",
// "zip": "15243",
// "city": "Unknownville"},
// {"email": "[email protected]"}
// ]
Fields with type option<'T>
are represented as a field that may or may not be there. This is the case both for unions and records.
[<NamedUnionCases>]
type Contact =
| Address of street: string * zip: string * city: string option
| Email of email: string
type User =
{
fullName: string
age: int option
contact: Contact
}
Content.Json
[
{
fullName = "John Doe"
age = Some 42
contact = Address("12 Random St.", "15243", Some "Unknownville")
}
{
fullName = "Jane Doe"
age = None
contact = Address("53 Alea St.", "51423", None)
}
]
// Output: [
// {"fullName": "John Doe",
// "age": 42,
// "contact":{"street": "12 Random St.",
// "zip": "15243",
// "city": "Unknownville"}},
// {"fullName": "Jane Doe",
// "contact":{"street": "53 Alea St.",
// "zip": "51423"}}
// ]
When parsing JSON, null
is also accepted as a None
value.
Union cases annotated with the attribute [<Constant c>]
are represented as the corresponding constant, which can be a string, int, float or bool. It is recommended to only use this attribute on argument-less cases. If all cases of a union are annotated with [<Constant>]
, then [<NamedUnionCases>]
is not necessary.
type Color =
| [<Constant "blue">] Blue
| [<Constant "red">] Red
| [<Constant "green">] Green
Content.Json [Blue; Red; Green]
// Output: ["blue","red","green"]
In order to be serializable to/from JSON on the server-side, a class must be annotated with the [<System.Serializable>]
attribute and must have a default constructor.
On the client-side, these are not checked or required.
Then, it is serialized based on its fields, similarly to F# records as mentioned above.
Here is an example in C#:
[Serializable]
public class User
{
Name name;
int age;
public User() { }
public User(Name name, int age)
{
this.name = name;
this.age = age;
}
}
[Serializable]
public class Name
{
[Name("first-name")] string firstName;
string lastName;
public Name() { }
public Name(string firstName, string lastName)
{
this.firstName = firstName;
this.lastName = lastName;
}
}
Then from F#:
Content.Json (User(Name("John", "Doe"), 36));
// Output: {"name": {"first-name": "John", "lastName": "Doe"}, "age": 36}
Values of type System.DateTime
are encoded using an ISO 8601 round-trip format string:
Content.Json System.DateTime.UtcNow
// Output: "2015-03-06T17:05:19.2077851Z"
The format can be customized with the attribute [<DateTimeFormat>]
.
This attribute can be placed either on a record field of type System.DateTime
or option<System.DateTime>
, or on a union case with an argument of one of these types.
type Action =
{
[<DateTimeFormat "yyyy-MM-dd">] dateOnly: System.DateTime
}
Content.Json { dateOnly = System.DateTime.UtcNow }
// Output: { dateOnly: "2015-03-24" }
[<NamedUnionCases>]
type Action =
| [<DateTimeFormat("time", "HH.mm.ss")>] A of time: System.DateTime
Content.Json (A (time = System.DateTime.UtcNow))
// Output: { time: "15.03.32" }
Note however that [<DateTimeFormat>]
is only available on the server side; this attribute is ignored by client-side serialization.