Skip to content

Commit 85ff4a5

Browse files
authored
Update documentation and move it to the docs folder (#76)
1 parent 02ac1e3 commit 85ff4a5

4 files changed

Lines changed: 885 additions & 344 deletions

File tree

README.md

Lines changed: 16 additions & 344 deletions
Original file line numberDiff line numberDiff line change
@@ -3,363 +3,31 @@
33
[![Build status](https://github.com/Tarmil/FSharp.SystemTextJson/workflows/Build/badge.svg)](https://github.com/Tarmil/FSharp.SystemTextJson/actions?query=workflow%3ABuild)
44
[![Nuget](https://img.shields.io/nuget/v/FSharp.SystemTextJson?logo=nuget)](https://nuget.org/packages/FSharp.SystemTextJson)
55

6-
This library provides F# union and record support for [System.Text.Json](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/).
6+
This library provides support for F# types to [System.Text.Json](https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-apis/).
77

8-
## Usage
8+
It adds support for the following types:
99

10-
The NuGet package is [`FSharp.SystemTextJson`](https://nuget.org/packages/FSharp.SystemTextJson). However the namespace is `System.Text.Json.Serialization` like the base library.
10+
* F# records (including struct records and anonymous records);
1111

12-
There are two ways to use `FSharp.SystemTextJson`: apply it to all F# types by passing `JsonSerializerOptions`, or apply it to specific types with an attribute.
12+
* F# discriminated unions (including struct unions), with a variety of representations;
1313

14-
### Using options
14+
* F# collections: `list<'T>`, `Map<'T>`, `Set<'T>`.
1515

16-
Add `JsonFSharpConverter` to the converters in `JsonSerializerOptions`, and the format will be applied to all F# types.
16+
It provides a number of customization options, allowing a wide range of JSON serialization formats.
1717

18-
```fsharp
19-
open System.Text.Json
20-
open System.Text.Json.Serialization
18+
## Documentation
2119

22-
let options = JsonSerializerOptions()
23-
options.Converters.Add(JsonFSharpConverter())
20+
* [How to use FSharp.SystemTextJson](docs/Using.md)
2421

25-
JsonSerializer.Serialize({| x = "Hello"; y = "world!" |}, options)
26-
// --> {"x":"Hello","y":"world!"}
27-
```
22+
* [Serialization format](docs/Format.md)
2823

29-
### Using attributes
30-
31-
Add `JsonFSharpConverterAttribute` to the type that needs to be serialized.
32-
33-
```fsharp
34-
open System.Text.Json
35-
open System.Text.Json.Serialization
36-
37-
[<JsonFSharpConverter>]
38-
type Example = { x: string; y: string }
39-
40-
JsonSerializer.Serialize({ x = "Hello"; y = "world!" })
41-
// --> {"x":"Hello","y":"world!"}
42-
```
43-
44-
### Advantages and inconvenients
45-
46-
The options way is generally recommended because it applies the format to all F# types. In addition to your defined types, this also includes:
47-
48-
* Types defined in referenced libraries that you can't modify to add an attribute. This includes standard library types such as `option` and `Result`.
49-
* Anonymous records.
50-
51-
The attribute way cannot handle the above cases.
52-
53-
The advantage of the attribute way is that it allows calling `Serialize` and `Deserialize` without having to pass options every time. This is particularly useful if you are passing your own data to a library that calls these functions itself and doesn't take options.
54-
55-
## Using with ASP.NET Core
56-
57-
ASP.NET Core can be easily configured to use FSharp.SystemTextJson.
58-
59-
### ASP.NET Core MVC
60-
61-
To use F# types in MVC controllers, add the following to your startup `ConfigureServices`:
62-
63-
```fsharp
64-
member this.ConfigureServices(services: IServiceCollection) =
65-
services.AddControllersWithViews() // or whichever method you're using to get an IMvcBuilder
66-
.AddJsonOptions(fun options ->
67-
options.JsonSerializerOptions.Converters.Add(JsonFSharpConverter()))
68-
|> ignore
69-
```
70-
71-
And you can then just do:
72-
73-
```fsharp
74-
type MyTestController() =
75-
inherit Controller()
76-
77-
member this.AddOne([<FromBody>] msg: {| value: int |}) =
78-
{| value = msg.value + 1 |}
79-
```
80-
81-
### SignalR
82-
83-
To use F# types in SignalR hubs, add the following to your startup `ConfigureServices`:
84-
85-
```fsharp
86-
member this.ConfigureServices(services: IServiceCollection) =
87-
services.AddSignalR()
88-
.AddJsonProtocol(fun options ->
89-
options.PayloadSerializerOptions.Converters.Add(JsonFSharpConverter()))
90-
|> ignore
91-
```
92-
93-
And you can then just do:
94-
95-
```fsharp
96-
type MyHub() =
97-
inherit Hub()
98-
99-
member this.AddOne(msg: {| value: int |})
100-
this.Clients.All.SendAsync("AddedOne", {| value = msg.value + 1 |})
101-
```
102-
103-
### Bolero
104-
105-
Since version 0.14, [Bolero](https://fsbolero.io) uses System.Text.Json and FSharp.SystemTextJson for its Remoting.
106-
107-
To use FSharp.SystemTextJson with its default options, there is nothing to do.
108-
109-
To customize FSharp.SystemTextJson, pass a function to the `AddRemoting` method in both the client-side and server-side setup.
110-
You can use the same function to ensure that both use the exact same options:
111-
112-
```fsharp
113-
// src/MyApp.Client/Startup.fs:
114-
115-
module Program =
116-
117-
// Customize here
118-
let serializerOptions (options: JsonSerializerOptions) =
119-
let converter = JsonFSharpConverter(JsonUnionEncoding.ThothLike)
120-
options.Converters.Add(converter)
121-
122-
[<EntryPoint>]
123-
let Main args =
124-
let builder = WebAssemblyHostBuilder.CreateDefault(args)
125-
builder.RootComponents.Add<Main.MyApp>("#main")
126-
builder.Services.AddRemoting(builder.HostEnvironment, serializerOptions) |> ignore
127-
builder.Build().RunAsync() |> ignore
128-
0
129-
130-
131-
// src/MyApp.Server/Startup.fs:
132-
133-
member this.ConfigureServices(services: IServiceCollection) =
134-
services.AddRemoting<MyRemoteService>(MyApp.Client.Program.serializerOptions) |> ignore
135-
// ...
136-
```
137-
138-
## Format
139-
140-
### Lists
141-
142-
F# lists are serialized as JSON arrays.
143-
144-
```fsharp
145-
JsonSerializer.Serialize([1; 2; 3], options)
146-
// --> [1,2,3]
147-
```
148-
149-
### Sets
150-
151-
F# sets are serialized as JSON arrays.
152-
153-
```fsharp
154-
JsonSerializer.Serialize(Set [1; 2; 3], options)
155-
// --> [1,2,3]
156-
```
157-
158-
### Maps
159-
160-
F# string-keyed maps are serialized as JSON objects.
161-
162-
```fsharp
163-
JsonSerializer.Serialize(Map [("a", 1); ("b", 2); ("c", 3)], options)
164-
// --> {"a":1,"b":2,"c":3}
165-
```
166-
167-
Maps with other types as keys are serialized as JSON arrays, where each item is a `[key,value]` array.
168-
169-
```fsharp
170-
JsonSerializer.Serialize(Map [(1, "a"); (2, "b"); (3, "c")], options)
171-
// --> [[1,"a"],[2,"b"],[3,"c"]]
172-
```
173-
174-
### Tuples
175-
176-
Tuples and struct tuples are serialized as JSON arrays.
177-
178-
```fsharp
179-
JsonSerializer.Serialize((1, "abc"), options)
180-
// --> [1,"abc"]
181-
182-
JsonSerializer.Serialize(struct (1, "abc"), options)
183-
// --> [1,"abc"]
184-
```
185-
186-
### Records and anonymous records
187-
188-
Records and anonymous records are serialized as JSON objects.
189-
190-
```fsharp
191-
type Example = { x: string; y: string }
192-
193-
JsonSerializer.Serialize({ x = "Hello"; y = "world!" }, options)
194-
// --> {"x":"Hello","y":"world!"}
195-
196-
JsonSerializer.Serialize({| x = "Hello"; y = "world!" |}, options)
197-
// --> {"x":"Hello","y":"world!"}
198-
```
199-
200-
Named record fields are serialized in the order in which they were declared in the type declaration.
201-
202-
Anonymous record fields are serialized in alphabetical order.
203-
204-
### Unions
205-
206-
Unions can be serialized in a number of formats. The enum `JsonUnionEncoding` defines the format to use; you can pass a value of this type to the constructor of `JsonFSharpConverter` or to the `JsonFSharpConverter` attribute.
207-
208-
```fsharp
209-
// Using options:
210-
let options = JsonSerializerOptions()
211-
options.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.UnwrapFieldlessTags))
212-
213-
// Using attributes:
214-
[<JsonFSharpConverter(JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.UnwrapFieldlessTags)>]
215-
type MyUnion = // ...
216-
```
217-
218-
<a name="union-field-names"></a>
219-
Some of the encoding styles for Unions write their union case name and/or argument values to a configurable tag.
220-
To configure this, `JsonFSharpConverter` accepts arguments called `unionTagName` and `unionFieldsName`, respectively.
221-
These tags default to `"Case"` and `"Fields"` respectively, and can be configured like so:
222-
223-
```fsharp
224-
// Using options:
225-
let options = JsonSerializerOptions()
226-
options.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.AdjacentTag ||| JsonUnionEncoding.NamedFields, "alternative-case-tag", "alternative-fields-tag"))
227-
228-
// Using attributes:
229-
[<JsonFSharpConverter(JsonUnionEncoding.AdjacentTag ||| JsonUnionEncoding.NamedFields, "alternative-case-tag", "alternative-fields-tag")>]
230-
type MyUnion = // ...
231-
```
232-
233-
Note that when using the default `JsonUnionEncoding`, `unionTagName` and `unionFieldsName` can be passed by key, rather than by position, e.g.:
234-
235-
``` fsharp
236-
[<JsonFSharpConverter(unionTagName="alternative-case-tag", unionFieldsName="alternative-fields-tag")>]
237-
type MyUnion = // ...
238-
```
239-
240-
Here are the possible values:
241-
242-
* `JsonUnionEncoding.AdjacentTag` is the default format. It represents unions as a JSON object with two fields:
243-
244-
* A field named from [`unionTagName`](#union-field-names): a string whose value is the name of the union case.
245-
* A field named from [`unionFieldsName`](#union-field-names): an array whose items are the arguments of the union case. This field is absent if the union case has no arguments.
246-
247-
This is the same format used by Newtonsoft.Json.
248-
249-
```fsharp
250-
type Example =
251-
| WithArgs of anInt: int * aString: string
252-
| NoArgs
253-
254-
JsonSerializer.Serialize(NoArgs, options)
255-
// --> {"Case":"NoArgs"}
256-
257-
JsonSerializer.Serialize(WithArgs (123, "Hello world!"), options)
258-
// --> {"Case":"WithArgs","Fields":[123,"Hello world!"]}
259-
```
260-
261-
* `JsonUnionEncoding.AdjacentTag ||| JsonUnionEncoding.NamedFields` is similar, except that the fields are represented as an object instead of an array. The field names on this object are the names of the arguments.
262-
263-
```fsharp
264-
JsonSerializer.Serialize(NoArgs, options)
265-
// --> {"Case":"NoArgs"}
266-
267-
JsonSerializer.Serialize(WithArgs (123, "Hello world!"), options)
268-
// --> {"Case":"WithArgs","Fields":{"anInt":123,"aString":"Hello world!"}}
269-
```
270-
271-
Note that if an argument doesn't have an explicit name, F# automatically gives it the name `Item` (if it's the only argument of its case) or `Item1`/`Item2`/etc (if the case has multiple arguments).
272-
273-
* `JsonUnionEncoding.ExternalTag` represents unions as a JSON object with one field, whose name is the name of the union case, and whose value is an array whose items are the arguments of the union case.
274-
275-
This is the same format used by FSharpLu.Json.
276-
277-
```fsharp
278-
JsonSerializer.Serialize(NoArgs, options)
279-
// --> {"NoArgs":[]}
280-
281-
JsonSerializer.Serialize(WithArgs (123, "Hello world!"), options)
282-
// --> {"WithArgs":[123,"Hello world!"]}
283-
```
284-
285-
* `JsonUnionEncoding.ExternalTag ||| JsonUnionEncoding.NamedFields` is similar, except that the fields are represented as an object instead of an array.
286-
287-
```fsharp
288-
JsonSerializer.Serialize(NoArgs, options)
289-
// --> {"NoArgs":{}}
290-
291-
JsonSerializer.Serialize(WithArgs (123, "Hello world!"), options)
292-
// --> {"WithArgs":{"anInt":123,"aString":"Hello world!"}}
293-
```
294-
295-
* `JsonUnionEncoding.InternalTag` represents unions as an array whose first item is the name of the case, and the rest of the items are the arguments.
296-
297-
This is the same format used by Thoth.Json.
298-
299-
```fsharp
300-
JsonSerializer.Serialize(NoArgs, options)
301-
// --> ["NoArgs"]
302-
303-
JsonSerializer.Serialize(WithArgs (123, "Hello world!"), options)
304-
// --> ["WithArgs",123,"Hello world!"]
305-
```
306-
307-
* `JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields` represents unions as an object whose first field is `unionTagName` (defaulting to `"Case"`), and whose value is the name of the case. The rest of the fields have the names and values of the arguments.
308-
309-
```fsharp
310-
JsonSerializer.Serialize(NoArgs, options)
311-
// --> {"Case":"NoArgs"}
312-
313-
JsonSerializer.Serialize(WithArgs (123, "Hello world!"), options)
314-
// --> {"Case":"WithArgs","anInt":123,"aString":"Hello world!"}
315-
```
316-
317-
* `JsonUnionEncoding.Untagged` represents unions as an object whose fields have the names and values of the arguments. The name of the case is not encoded at all. Deserialization is only possible if the fields of all cases have different names.
318-
319-
320-
```fsharp
321-
JsonSerializer.Serialize(NoArgs, options)
322-
// --> {}
323-
324-
JsonSerializer.Serialize(WithArgs (123, "Hello world!"), options)
325-
// --> {"anInt":123,"aString":"Hello world!"}
326-
```
327-
328-
* Additionally, or-ing `||| JsonUnionEncoding.UnwrapFieldlessTags` to any of the previous formats represents cases that don't have any arguments as a simple string.
329-
330-
```fsharp
331-
JsonSerializer.Serialize(NoArgs, options)
332-
// --> "NoArgs"
333-
334-
JsonSerializer.Serialize(WithArgs (123, "HelloWorld!"), options)
335-
// --> (same format as without UnwrapFieldlessTags)
336-
```
337-
338-
Union cases that are represented as `null` in .NET using `CompilationRepresentationFlags.UseNullAsTrueValue`, such as `Option.None`, are serialized as `null`.
339-
340-
### Options
341-
342-
By default, the type `'T option` is treated specially.
343-
344-
* The value `None`, as mentioned above, is represented as `null`.
345-
346-
* The value `Some x` is represented the same as `x`, without wrapping it in the union representation for `Some`.
347-
348-
When using a custom `JsonUnionEncoding`, this behavior is enabled by or-ing `||| JsonUnionEncoding.UnwrapOption`.
24+
* [Customizing the format](docs/Customizing.md)
34925

35026
## FAQ
35127

352-
* Does FSharp.SystemTextJson support struct records and unions?
353-
354-
Yes!
355-
356-
* Does FSharp.SystemTextJson support anonymous records?
357-
358-
Yes!
359-
36028
* Does FSharp.SystemTextJson support alternative formats for unions?
36129

362-
[Yes!](#unions)
30+
[Yes!](docs/Customizing.md)
36331

36432
* Does FSharp.SystemTextJson support representing `'T option` as either just `'T` or `null` (or an absent field)?
36533

@@ -374,10 +42,14 @@ Yes!
37442

37543
Yes!
37644

45+
* Can I customize the format for a specific type?
46+
47+
[Yes!](docs/Customizing.md#how-to-apply-customizations)
48+
37749
* Does FSharp.SystemTextJson allocate memory?
37850

37951
As little as possible, but unfortunately the `FSharp.Reflection` API requires some allocations. In particular, an array is allocated for as many items as the record fields or union arguments, and structs are boxed. There is [work in progress](https://github.com/Tarmil/FSharp.SystemTextJson/pull/15) to improve this.
38052

38153
* Are there any benchmarks, e.g. against Newtonsoft.Json?
38254

383-
[Yes!](https://github.com/Tarmil/FSharp.SystemTextJson/pull/11)
55+
[Yes!](https://github.com/Tarmil/FSharp.SystemTextJson/pull/11)

0 commit comments

Comments
 (0)