Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ The `Unreleased` section name is replaced by the expected version of next releas
## [Unreleased]

### Added

- `SystemTextJson.Options.CreateWeb`: New method to create web-optimized serializer options based on `JsonSerializerOptions.Web` (STJ 10+) with camelCase naming, case-insensitive deserialization, and number-from-string support [#129](https://github.com/jet/FsCodec/pull/129)

### Changed

- `SystemTextJson`: Upped minimum `System.Text.Json` version to `10.0.` [#129](https://github.com/jet/FsCodec/pull/129)
- `SystemTextJson.UnionConverter`: Optimized to use `WriteRawValue` for better performance when writing JSON (STJ 10+) [#129](https://github.com/jet/FsCodec/pull/129)

### Removed
### Fixed
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ The respective concrete Codec packages include relevant `Converter`/`JsonConvert
- `(autoTypeSafeEnumToJsonString = true)`: triggers usage of `TypeSafeEnumConverter` for any F# Discriminated Unions that only contain nullary cases. See [`AutoUnionTests.fs`](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs) for examples
- `(autoUnionToJsonObject = true)`: triggers usage of a `UnionConverter` to round-trip F# Discriminated Unions (with at least a single case that has a body) as JSON Object structures. See [`AutoUnionTests.fs`](https://github.com/jet/FsCodec/blob/master/tests/FsCodec.SystemTextJson.Tests/AutoUnionTests.fs) for examples
- `(rejectNullStrings = true)`: triggers usage of [`RejectNullStringConverter`](https://github.com/jet/FsCodec/blob/master/src/FsCodec.SystemTextJson/RejectNullStringConverter.fs) to reject `null` as a value for strings (`string option` can be used to handle them explicitly).
- `CreateWeb`: _(System.Text.Json 10+)_ creates web-optimized serializer options based on `JsonSerializerOptions.Web`. Provides camelCase property naming, case-insensitive property matching for deserialization, and allows reading numbers from strings - optimized for typical web API scenarios. Supports the same converter options as `Create` (`autoTypeSafeEnumToJsonString`, `autoUnionToJsonObject`, `rejectNullStrings`).
- `Default`: Default settings; same as calling `Create()` produces (same intent as [`JsonSerializerOptions.Default`](https://github.com/dotnet/runtime/pull/61434))

## `Serdes`
Expand Down
43 changes: 43 additions & 0 deletions src/FsCodec.SystemTextJson/Options.fs
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,46 @@ type Options private () =
?indent = indent,
?camelCase = camelCase,
unsafeRelaxedJsonEscaping = defaultArg unsafeRelaxedJsonEscaping true)

/// <summary>Creates web-optimized serializer options based on <c>JsonSerializerOptions.Web</c> from System.Text.Json 10+. <br/>
/// Features of <c>JsonSerializerOptions.Web</c>: <br/>
/// - camelCase property naming <br/>
/// - case-insensitive property matching for deserialization <br/>
/// - allows reading numbers from strings <br/>
/// - optimized for typical web API scenarios <br/>
/// This method allows adding custom converters and overriding specific settings while maintaining the web-optimized base configuration.</summary>
static member CreateWeb
( // List of converters to apply. Implicit converters may be prepended and/or be used as a default
[<Optional; ParamArray>] converters: JsonConverter[],
// Use multi-line, indented formatting when serializing JSON; defaults to false.
[<Optional; DefaultParameterValue(null)>] ?indent: bool,
// Ignore null values in input data, don't render fields with null values; defaults to `false`.
[<Optional; DefaultParameterValue(null)>] ?ignoreNulls: bool,
// Apply <c>TypeSafeEnumConverter</c> if possible. Defaults to <c>false</c>.
[<Optional; DefaultParameterValue(null)>] ?autoTypeSafeEnumToJsonString: bool,
// Apply <c>UnionConverter</c> for all Discriminated Unions, if <c>TypeSafeEnumConverter</c> not possible. Defaults to <c>false</c>.
[<Optional; DefaultParameterValue(null)>] ?autoUnionToJsonObject: bool,
// Apply <c>RejectNullStringConverter</c> in order to have serialization throw on <c>null</c> strings.
// Use <c>string option</c> to represent strings that can potentially be <c>null</c>.
[<Optional; DefaultParameterValue(null)>] ?rejectNullStrings: bool) =
let autoTypeSafeEnumToJsonString = defaultArg autoTypeSafeEnumToJsonString false
let autoUnionToJsonObject = defaultArg autoUnionToJsonObject false
let rejectNullStrings = defaultArg rejectNullStrings false
let indent = defaultArg indent false
let ignoreNulls = defaultArg ignoreNulls false

// Start with JsonSerializerOptions.Web which provides optimized defaults for web scenarios
let options = JsonSerializerOptions(JsonSerializerOptions.Web)

// Add custom converters
[| if rejectNullStrings then yield (RejectNullStringConverter() :> JsonConverter)
if autoTypeSafeEnumToJsonString || autoUnionToJsonObject then
yield (UnionOrTypeSafeEnumConverterFactory(typeSafeEnum = autoTypeSafeEnumToJsonString, union = autoUnionToJsonObject) :> JsonConverter)
if converters <> null then yield! converters |]
|> Array.iter options.Converters.Add

// Apply overrides
if indent then options.WriteIndented <- true
if ignoreNulls then options.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull

options
9 changes: 6 additions & 3 deletions src/FsCodec.SystemTextJson/UnionConverter.fs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,17 @@ type UnionConverter<'T>() =
let fieldValues = case.deconstruct value
for fieldInfo, fieldValue in Seq.zip case.fields fieldValues do
if fieldValue <> null || options.DefaultIgnoreCondition <> Serialization.JsonIgnoreCondition.Always then
let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options)
if case.fields.Length = 1 && FSharpType.IsRecord(fieldInfo.PropertyType, true) then
// flatten the record properties into the same JSON object as the discriminator
// Use WriteRawValue for better performance (STJ 10+)
let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options)
for prop in element.EnumerateObject() do
prop.WriteTo writer
writer.WritePropertyName(prop.Name)
writer.WriteRawValue(prop.Value.GetRawText())
else
writer.WritePropertyName(fieldInfo.Name)
element.WriteTo writer
let element = JsonSerializer.SerializeToElement(fieldValue, fieldInfo.PropertyType, options)
writer.WriteRawValue(element.GetRawText())
writer.WriteEndObject()

override _.Read(reader, t: Type, options) =
Expand Down