Skip to content

Commit 43a6d14

Browse files
committed
Document JsonNameAttribute
1 parent 5f3769b commit 43a6d14

2 files changed

Lines changed: 156 additions & 52 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ To supersede it, use an explicit `JsonUnionEncoding` that does not include `Unwr
3636

3737
* Does FSharp.SystemTextJson support `JsonPropertyNameAttribute` and `JsonIgnoreAttribute` on record fields?
3838

39-
Yes!
39+
Yes! It also provides [a more powerful `JsonNameAttribute`](docs/Customizing.md#jsonname) that supports non-string union tags.
4040

4141
* Does FSharp.SystemTextJson support options such as `PropertyNamingPolicy` and `IgnoreNullValues`?
4242

43-
Yes!
43+
Yes! It also supports naming policy [for union tags](docs/Customizing.md#uniontagnamingpolicy).
4444

4545
* Can I customize the format for a specific type?
4646

47-
[Yes!](docs/Customizing.md#how-to-apply-customizations)
47+
[Yes!](docs/Customizing.md#how-to-apply-options)
4848

4949
* Does FSharp.SystemTextJson allocate memory?
5050

docs/Customizing.md

Lines changed: 153 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,46 @@
11
# Customizing the serialization format
22

3+
The serialization and deserialization of `FSharp.SystemTextJson` can be customized in two ways: using global options, and by adding attributes to specific types and properties.
4+
35
<!-- doctoc --github docs/Customizing.md -->
46
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
57
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
68

7-
- [How to apply customizations](#how-to-apply-customizations)
8-
- [`unionEncoding`](#unionencoding)
9-
- [Base encoding](#base-encoding)
10-
- [`AdjacentTag`](#adjacenttag)
11-
- [`ExternalTag`](#externaltag)
12-
- [`InternalTag`](#internaltag)
13-
- [`Untagged`](#untagged)
14-
- [Additional options](#additional-options)
15-
- [`NamedFields`](#namedfields)
16-
- [`UnwrapFieldlessTags`](#unwrapfieldlesstags)
17-
- [`UnwrapOption`](#unwrapoption)
18-
- [`UnwrapSingleCaseUnions`](#unwrapsinglecaseunions)
19-
- [`UnwrapSingleFieldCases`](#unwrapsinglefieldcases)
20-
- [`UnwrapRecordCases`](#unwraprecordcases)
21-
- [`UnionFieldNamesFromTypes`](#unionfieldnamesfromtypes)
22-
- [`AllowUnorderedTag`](#allowunorderedtag)
23-
- [Combined flags](#combined-flags)
24-
- [`unionTagName`](#uniontagname)
25-
- [`unionFieldsName`](#unionfieldsname)
26-
- [`unionTagNamingPolicy`](#uniontagnamingpolicy)
27-
- [`unionFieldNamingPolicy`](#unionfieldnamingpolicy)
28-
- [`unionTagCaseInsensitive`](#uniontagcaseinsensitive)
29-
- [`includeRecordProperties`](#includerecordproperties)
30-
- [`allowNullFields`](#allownullfields)
9+
- [Global options](#global-options)
10+
- [How to apply options](#how-to-apply-options)
11+
- [`unionEncoding`](#unionencoding)
12+
- [Base encoding](#base-encoding)
13+
- [`AdjacentTag`](#adjacenttag)
14+
- [`ExternalTag`](#externaltag)
15+
- [`InternalTag`](#internaltag)
16+
- [`Untagged`](#untagged)
17+
- [Additional options](#additional-options)
18+
- [`NamedFields`](#namedfields)
19+
- [`UnwrapFieldlessTags`](#unwrapfieldlesstags)
20+
- [`UnwrapOption`](#unwrapoption)
21+
- [`UnwrapSingleCaseUnions`](#unwrapsinglecaseunions)
22+
- [`UnwrapSingleFieldCases`](#unwrapsinglefieldcases)
23+
- [`UnwrapRecordCases`](#unwraprecordcases)
24+
- [`UnionFieldNamesFromTypes`](#unionfieldnamesfromtypes)
25+
- [`AllowUnorderedTag`](#allowunorderedtag)
26+
- [Combined flags](#combined-flags)
27+
- [`unionTagName`](#uniontagname)
28+
- [`unionFieldsName`](#unionfieldsname)
29+
- [`unionTagNamingPolicy`](#uniontagnamingpolicy)
30+
- [`unionFieldNamingPolicy`](#unionfieldnamingpolicy)
31+
- [`unionTagCaseInsensitive`](#uniontagcaseinsensitive)
32+
- [`includeRecordProperties`](#includerecordproperties)
33+
- [`allowNullFields`](#allownullfields)
34+
- [`types`](#types)
35+
- [Attributes](#attributes)
36+
- [JsonFSharpConverter](#jsonfsharpconverter)
37+
- [JsonName](#jsonname)
3138

3239
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
3340

34-
## How to apply customizations
41+
## Global options
42+
43+
### How to apply options
3544

3645
The way to customize the serialization format depends on [how FSharp.SystemTextJson is used](Using.md).
3746

@@ -76,7 +85,7 @@ The way to customize the serialization format depends on [how FSharp.SystemTextJ
7685
| MyOtherCase of string
7786
```
7887
79-
## `unionEncoding`
88+
### `unionEncoding`
8089
8190
The customization option `unionEncoding` defines the format used to encode discriminated unions.
8291
Its type is `JsonUnionEncoding`, and it is an enum of flags that can be combined using the binary "or" operator (`|||`).
@@ -107,12 +116,12 @@ type Example =
107116
| WithArgs of anInt: int * aString: string
108117
```
109118

110-
### Base encoding
119+
#### Base encoding
111120

112121
There are four base encodings available.
113122
These encodings and their names are inspired by Rust's excellent Serde library, although they differ in some specifics.
114123

115-
#### `AdjacentTag`
124+
##### `AdjacentTag`
116125

117126
`JsonUnionEncoding.AdjacentTag` is the default format.
118127

@@ -137,7 +146,7 @@ JsonSerializer.Serialize(WithArgs (123, "Hello, world!"), options)
137146

138147
The names `"Case"` and `"Fields"` can be customized (see [`unionTagName`](#uniontagname) and [`unionFieldsName`](#unionfieldsname) below).
139148

140-
#### `ExternalTag`
149+
##### `ExternalTag`
141150

142151
`JsonUnionEncoding.ExternalTag` represents unions similarly to FSharpLu.Json.
143152
A union value is serialized as a JSON object with one field, whose name is the name of the union case, and whose value is an array.
@@ -153,7 +162,7 @@ JsonSerializer.Serialize(WithArgs (123, "Hello, world!"), options)
153162
// --> {"WithArgs":[123,"Hello, world!"]}
154163
```
155164

156-
#### `InternalTag`
165+
##### `InternalTag`
157166

158167
`JsonUnionEncoding.InternalTag` represents unions similarly to Thoth.Json.
159168
A union value is serialized as a JSON array whose first item is the name of the case, and the rest are its fields.
@@ -169,7 +178,7 @@ JsonSerializer.Serialize(WithArgs (123, "Hello, world!"), options)
169178
// --> ["WithArgs",123,"Hello, world!"]
170179
```
171180

172-
#### `Untagged`
181+
##### `Untagged`
173182

174183
`JsonUnionEncoding.Untagged` represents unions as an object whose fields have the names and values of the union's fields.
175184
The name of the case is not encoded at all.
@@ -188,9 +197,9 @@ JsonSerializer.Serialize(WithArgs (123, "Hello, world!"), options)
188197

189198
This flag also sets the `NamedFields` additional flag (see [below](#additional-options)).
190199

191-
### Additional options
200+
#### Additional options
192201

193-
#### `NamedFields`
202+
##### `NamedFields`
194203

195204
`JsonUnionEncoding.NamedFields` causes the fields of a union to be encoded as a JSON object rather than an array.
196205
The properties of the object are named after the value's fields (`aFloat`, `anInt` and `aString` in our example).
@@ -257,7 +266,7 @@ Its exact effect depends on the base format:
257266
If a field doesn't have a name specified in F# code, then a default name is assigned by the compiler:
258267
`Item` if the case has a single field, and `Item1`, `Item2`, etc if the case has multiple fields.
259268
260-
#### `UnwrapFieldlessTags`
269+
##### `UnwrapFieldlessTags`
261270
262271
`JsonUnionEncoding.UnwrapFieldlessTags` represents cases that don't have any fields as a simple string.
263272
@@ -272,7 +281,7 @@ JsonSerializer.Serialize(WithArgs (123, "Hello, world!"), options)
272281
// --> (same format as without UnwrapFieldlessTags)
273282
```
274283

275-
#### `UnwrapOption`
284+
##### `UnwrapOption`
276285

277286
`JsonUnionEncoding.UnwrapOption` is enabled by default.
278287
It causes the types `'T option` and `'T voption` (aka `ValueOption`) to be treated specially.
@@ -283,7 +292,7 @@ It causes the types `'T option` and `'T voption` (aka `ValueOption`) to be treat
283292
Combined with the option `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull` on `JsonSerializerOptions`, this can be used to represent optional fields: `Some` is a value that is present in the JSON object, and `None` is a value that is absent from the JSON object.
284293
Note that the same effect can also be achieved more explicitly and more safely using [the `Skippable` type](#skippable).
285294

286-
#### `UnwrapSingleCaseUnions`
295+
##### `UnwrapSingleCaseUnions`
287296

288297
`JsonUnionEncoding.UnwrapSingleCaseUnions` is enabled by default.
289298
It causes unions that have a single case with a single field to be treated as simple "wrappers", and serialized as their single field's value.
@@ -304,7 +313,7 @@ JsonSerializer.Serialize(UserId "tarmil", options)
304313
// --> "tarmil"
305314
```
306315

307-
#### `UnwrapSingleFieldCases`
316+
##### `UnwrapSingleFieldCases`
308317

309318
`JsonUnionEncoding.UnwrapSingleFieldCases`: if a union case has a single field, it is not wrapped in an array or object.
310319
The exact effect depends on the base format:
@@ -343,7 +352,7 @@ The exact effect depends on the base format:
343352
344353
* `JsonUnionEncoding.Untagged ||| JsonUnionEncoding.UnwrapSingleFieldCases`: no effect.
345354
346-
#### `UnwrapRecordCases`
355+
##### `UnwrapRecordCases`
347356
348357
`JsonUnionEncoding.UnwrapRecordCases` implicitly sets `NamedFields` (see above).
349358
If a union case has a single field which is a record, then this record's fields are used directly as the fields of the object representing the union.
@@ -408,7 +417,7 @@ type Location =
408417
// Instead of {"Item":{"lat":48.858,"long":2.295}}
409418
```
410419
411-
#### `UnionFieldNamesFromTypes`
420+
##### `UnionFieldNamesFromTypes`
412421
413422
When using `NamedFields`, if a field doesn't have a name specified in F# code, then a default name is assigned by the compiler:
414423
`Item` if the case has a single field, and `Item1`, `Item2`, etc if the case has multiple fields.
@@ -431,7 +440,7 @@ JsonSerializer.Serialize(Pair(123, "test"), options)
431440
```
432441

433442

434-
#### `AllowUnorderedTag`
443+
##### `AllowUnorderedTag`
435444

436445
`JsonUnionEncoding.AllowUnorderedTag` is enabled by default.
437446
It takes effect during deserialization in AdjacentTag and InternalTag modes.
@@ -452,7 +461,7 @@ JsonSerializer.Deserialize("""{"Fields":[3.14],"Case":"WithOneArg"}""", options)
452461
// --> WithOneArg 3.14
453462
```
454463

455-
### Combined flags
464+
#### Combined flags
456465

457466
`JsonUnionEncoding` also contains a few items that combine several of the above flags.
458467

@@ -500,7 +509,7 @@ JsonSerializer.Deserialize("""{"Fields":[3.14],"Case":"WithOneArg"}""", options)
500509
||| JsonUnionEncoding.AllowUnorderedTag
501510
```
502511
503-
## `unionTagName`
512+
### `unionTagName`
504513
505514
This option sets the name of the property that contains the union case.
506515
This affects the base encodings `AdjacentTag` and `InternalTag ||| NamedFields`.
@@ -514,7 +523,7 @@ JsonSerializer.Serialize(WithArgs (123, "Hello, world!"), options)
514523
// --> {"type":"WithArgs","Fields":[123,"Hello, world!"]}
515524
```
516525

517-
## `unionFieldsName`
526+
### `unionFieldsName`
518527

519528
This option sets the name of the property that contains the union fields.
520529
This affects the base encoding `AdjacentTag`.
@@ -528,7 +537,7 @@ JsonSerializer.Serialize(WithArgs (123, "Hello, world!"), options)
528537
// --> {"Case":"WithArgs","value":[123,"Hello, world!"]}
529538
```
530539

531-
## `unionTagNamingPolicy`
540+
### `unionTagNamingPolicy`
532541

533542
This option sets the naming policy for union case names.
534543
See [the System.Text.Json documentation about naming policies](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-customize-properties).
@@ -543,7 +552,7 @@ JsonSerializer.Serialize(WithArgs(123, "Hello, world!"), options)
543552

544553
When using the attribute, this option has enum type `JsonKnownNamingPolicy` instead.
545554

546-
## `unionFieldNamingPolicy`
555+
### `unionFieldNamingPolicy`
547556

548557
This option sets the naming policy for union field names.
549558
See [the System.Text.Json documentation about naming policies](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-customize-properties).
@@ -563,7 +572,7 @@ JsonSerializer.Serialize(Person("John", "Doe"), options)
563572

564573
When using the attribute, this option has enum type `JsonKnownNamingPolicy` instead.
565574

566-
## `unionTagCaseInsensitive`
575+
### `unionTagCaseInsensitive`
567576

568577
This option only affects deserialization.
569578
It makes the parsing of union case names case-insensitive.
@@ -576,7 +585,7 @@ JsonSerializer.Deserialize<Example>("""{"Case":"wIThArgS","Fields":[123,"Hello,
576585
// --> WithArgs (123, "Hello, world!")
577586
```
578587

579-
## `includeRecordProperties`
588+
### `includeRecordProperties`
580589

581590
By default, only record fields are serialized. When `includeRecordProperties` is set to `true`, record properties are serialized as well.
582591

@@ -615,7 +624,7 @@ JsonSerializer.Serialize({ Width = 4.; Height = 5. }, options)
615624
// --> {"Width":4,"Height":5,"Area":20}
616625
```
617626

618-
## `allowNullFields`
627+
### `allowNullFields`
619628

620629
By default, FSharp.SystemTextJson throws an exception when the following conditions are met:
621630

@@ -643,7 +652,7 @@ JsonSerializer.Deserialize<Rectangle>("""{"TopRight":{"X":1,"Y":2}}""", options)
643652
// --> { BottomLeft = null; TopRight = Point(X = 1., Y = 2.) }
644653
```
645654

646-
## `types`
655+
### `types`
647656

648657
Since the first release of `FSharp.SystemTextJson`, the base library `System.Text.Json` has added support for a number of F# types, including:
649658

@@ -671,3 +680,98 @@ It also includes a few combined flags:
671680
* `Collections`: lists, sets and maps.
672681
* `Minimal`: All types not already fully supported by `System.Text.Json`. As of `FSharp.SystemTextJson` on .NET 6, this includes maps (for complex key types), unions and tuples (for tuples of more than 7 items).
673682
* `All`: this is the default, `FSharp.SystemTextJson` handles all the types it supports.
683+
684+
## Attributes
685+
686+
### JsonFSharpConverter
687+
688+
The attribute `JsonFSharpConverterAttribute` on a type indicates that this type must be serialized using `FSharp.SystemTextJson`. See [Using attributes](Using.md#using-attributes) for applying it, and [How to apply options](#how-to-apply-options) for specializing global options on a type using this attribute.
689+
690+
### JsonName
691+
692+
`FSharp.SystemTextJson` supports the standard `JsonPropertyNameAttribute` to define the name of a property in JSON.
693+
694+
```fsharp
695+
type Example =
696+
{ [<JsonPropertyName "thisIsX">] x: string
697+
y: string }
698+
699+
JsonSerializer.Serialize({ x = "Hello"; y = "world!" }, options)
700+
// --> {"thisIsX":"Hello","y":"world!"}
701+
```
702+
703+
However, it also includes its own attribute `JsonNameAttribute` which provides more functionality.
704+
705+
* It can be used exactly like `JsonPropertyNameAttribute`:
706+
707+
```fsharp
708+
type Example =
709+
{ [<JsonName "thisIsX">] x: string
710+
y: string }
711+
712+
JsonSerializer.Serialize({ x = "Hello"; y = "world!" }, options)
713+
// --> {"thisIsX":"Hello","y":"world!"}
714+
```
715+
716+
* It can be used with multiple values. In this case, all the values are recognized as field names for deserialization. The first value is used for serialization.
717+
718+
```fsharp
719+
type Example =
720+
{ [<JsonName("thisIsX", "reallyX")>] x: string
721+
y: string }
722+
723+
JsonSerializer.Deserialize("""{"thisIsX":"Hello","y":"world!"}""", options)
724+
// --> { x = "Hello"; y = "world!" }
725+
726+
JsonSerializer.Deserialize("""{"reallyX":"Hello","y":"world!"}""", options)
727+
// --> { x = "Hello"; y = "world!" }
728+
729+
JsonSerializer.Serialize({ x = "Hello"; y = "world!" }, options)
730+
// --> {"thisIsX":"Hello","y":"world!"}
731+
```
732+
733+
* It can be used on a discriminated union case to determine the tag of this case. In this situation, the value can be a string, an integer or a boolean.
734+
735+
```fsharp
736+
type Example =
737+
| [<JsonName 1>] One of int
738+
| [<JsonName 2>] Two of string
739+
740+
JsonSerializer.Serialize(Two "hello", options)
741+
// --> {"Case":2,"Fields":["hello"]}
742+
```
743+
744+
Combined with `JsonUnionEncoding.InternalTag`, this is a way to encode the common pattern in JSON where the value of one field determines what other fields are allowed:
745+
746+
```fsharp
747+
[<JsonFSharpConverter(JsonUnionEncoding.Default
748+
||| JsonUnionEncoding.InternalTag
749+
||| JsonUnionEncoding.NamedFields,
750+
unionTagName = "isSuccess")>]
751+
type MyResult<'t> =
752+
| [<JsonName false>] Error of message: string
753+
| [<JsonName true>] Ok of 't
754+
755+
JsonSerializer.Serialize(Ok {| x = 1; y = "hello" |})
756+
// --> {"isSuccess":true,"x":1,"y":"hello"}
757+
758+
JsonSerializer.Serialize(Error "Failed to retrieve x")
759+
// --> {"isSuccess":false,"message":"Failed to retrieve x"}
760+
```
761+
762+
* Using the `Field` property, it can be used to indicate the name(s) of a discriminated case field.
763+
764+
```fsharp
765+
[<JsonFSharpConverter(JsonUnionEncoding.Default
766+
||| JsonUnionEncoding.InternalTag
767+
||| JsonUnionEncoding.NamedFields,
768+
unionTagName = "isSuccess")>]
769+
type MyResult<'t> =
770+
| [<JsonName false>]
771+
[<JsonName("error", "errorMessage", Field = "message")]
772+
Error of message: string
773+
| [<JsonName true>] Ok of 't
774+
775+
JsonSerializer.Serialize(Error "Failed to retrieve x")
776+
// --> {"isSuccess":false,"error":"Failed to retrieve x"}
777+
```

0 commit comments

Comments
 (0)