#
JSON format
This page describes how F# values (and C# objects) are converted to JSON by the functions available in WebSharper.Json
.
#
Customization
If you would like to customize the default JSON representation, the following attributes are available (see examples in the sections below):
[<Name>]
to customize field names in F#records and union cases indiscriminated unions (DUs).[<NamedUnionCases>]
to customize the name of thediscriminator field that determines which DU shape the value holds.[<Constant>]
to represent DU cases asconstants/literals .[<DateTimeFormat>]
(server-side only) to customize the format used for .System.DateTime
values[<System.Serializable>]
to markC#/F# classes to be JSON serializable.
#
Base types
The following base types are handled:
- signed and unsigned integers:
uint8 (byte)
/int8
,uint16
/int16
,uint32
/int32
(int
),uint64
/int64
- floats:
single
,double (float)
- decimals:
decimal
- strings:
string
- booleans:
bool
#
Collections
- Values of type
list<'T>
,'T[]
andSet<'T>
are represented as JSON arrays. - Values of type
Map<string, 'T>
andSystem.Collections.Generic.Dictionary<string, 'T>
are represented as flat JSON objects. - Other
Map
andDictionary
values are represented as an array of key-value pairs.
#
Tuples
Tuples (including struct tuples) are also represented as JSON arrays:
#
Records
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 }
{"name": {"first-name": "John", "LastName": "Doe"}, "age": 42}
#
Unions
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.
#
Explicit discriminator
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]"
]
[
{"kind": "address",
"street": "12 Random St.",
"zip": "15243",
"city": "Unknownville"},
{"kind": "Email",
"email": "[email protected]"}
]
Unnamed arguments receive the names Item1
, Item2
, etc.
Missing the [<NamedUnionCases>]
attribute, the case names 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]"
]
[
{"$": 0,
"street": "12 Random St.",
"zip": "15243",
"city": "Unknownville"},
{"$": 1,
"email": "[email protected]"}
]
#
Implicit discriminator
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]"
]
[
{"street": "12 Random St.",
"zip": "15243",
"city": "Unknownville"},
{"email": "[email protected]"}
]
#
Record inside union
As a special case, if a union case has a single, unnamed record argument, 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]"
]
[
{"street": "12 Random St.",
"zip": "15243",
"city": "Unknownville"},
{"email": "[email protected]"}
]
#
Optional fields
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)
}
]
[
{"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.
#
Constant cases
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]
["blue","red","green"]
#
Classes
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
[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;
}
}
Content.Json(User(Name("John", "Doe"), 36))
{"name": {"first-name": "John", "lastName": "Doe"}, "age": 36}
#
DateTimes
Values of type System.DateTime
are encoded using an ISO 8601 round-trip format string:
Content.Json System.DateTime.UtcNow
"2015-03-06T17:05:19.2077851Z"
The format can be customized with the [<DateTimeFormat>]
attribute. 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.
open System
type MyType =
{
[<DateTimeFormat "yyyy-MM-dd">] DateOnly:DateTime
}
Content.Json { DateOnly = DateTime.UtcNow }
{ DateOnly: "2015-03-24" }
[<NamedUnionCases>]
type MyTime =
| [<DateTimeFormat("time", "HH.mm.ss")>] Time of time:DateTime
Content.Json (Time DateTime.UtcNow)
{ time: "15.03.32" }
Note, however, that [<DateTimeFormat>]
is only available on the server side; this attribute is ignored by client-side serialization.