Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Promote System.Text.Json as default JSON serializer #563

Merged
merged 8 commits into from
May 18, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
143 changes: 89 additions & 54 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -2933,59 +2933,7 @@ Please visit the [Giraffe.ViewEngine](https://github.com/giraffe-fsharp/Giraffe.

### JSON

By default Giraffe uses [Newtonsoft.Json](https://www.newtonsoft.com/json) for (de-)serializing JSON content. An application can modify the default serializer by registering a new dependency which implements the `Json.ISerializer` interface during application startup.

Customizing Giraffe's JSON serialization can either happen via providing a custom object of `JsonSerializerSettings` when instantiating the default `NewtonsoftJson.Serializer` or by swapping in an entire different JSON library by creating a new class which implements the `Json.ISerializer` interface.

By default Giraffe offers three `Json.ISerializer` implementations out of the box:

| Name | Description | Default |
| :--- | :---------- | :------ |
| `NewtonsoftJson.Serializer` | Uses `Newtonsoft.Json` aka Json.NET for JSON (de-)serialization in Giraffe. It is the most downloaded library on NuGet, battle tested by millions of users and has great support for F# data types. Use this json serializer for maximum compatibility and easy adoption. | True |
| `SystemTextJson.Serializer` | Uses `System.Text.Json` for JSON (de-)serialization in Giraffe. `System.Text.Json` is a high performance serialization library, and aims to be the serialization library of choice for ASP.NET Core. For better support of F# types with `System.Text.Json`, look at [FSharp.SystemTextJson](https://github.com/Tarmil/FSharp.SystemTextJson). | False |

To use `SystemTextJson.Serializer` instead of `NewtonsoftJson.Serializer`, register a new dependency of type `Json.ISerializer` during application configuration:

```fsharp
let configureServices (services : IServiceCollection) =
// First register all default Giraffe dependencies
services.AddGiraffe() |> ignore

let serializationOptions = SystemTextJson.Serializer.DefaultOptions
// Optionally use `FSharp.SystemTextJson` (requires `FSharp.SystemTextJson` package reference)
serializationOptions.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.FSharpLuLike))
// Now register SystemTextJson.Serializer
services.AddSingleton<Json.ISerializer>(SystemTextJson.Serializer(serializationOptions)) |> ignore
```

#### Customizing JsonSerializerSettings
dbrattli marked this conversation as resolved.
Show resolved Hide resolved

You can change the default `JsonSerializerSettings` of the `NewtonsoftJson.Serializer` by registering a new instance of `NewtonsoftJson.Serializer` during application startup. For example, the [`Microsoft.FSharpLu` project](https://github.com/Microsoft/fsharplu/wiki/fsharplu.json) provides a Json.NET converter (`CompactUnionJsonConverter`) that serializes and deserializes `Option`s and discriminated unions much more succinctly. If you wanted to use it, and set the culture to German, your configuration would look something like:

```fsharp
let configureServices (services : IServiceCollection) =
// First register all default Giraffe dependencies
services.AddGiraffe() |> ignore

// Now customize only the Json.ISerializer by providing a custom
// object of JsonSerializerSettings
let customSettings = JsonSerializerSettings(
Culture = CultureInfo("de-DE"))
customSettings.Converters.Add(CompactUnionJsonConverter(true))

services.AddSingleton<Json.ISerializer>(
NewtonsoftJson.Serializer(customSettings)) |> ignore

[<EntryPoint>]
let main _ =
WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
```
By default Giraffe uses `System.Text.Json` for (de-)serializing JSON content. An application can modify the default serializer by registering a new dependency which implements the `Json.ISerializer` interface during application startup.
esbenbjerre marked this conversation as resolved.
Show resolved Hide resolved

#### Using a different JSON serializer

Expand All @@ -3004,6 +2952,67 @@ type CustomJsonSerializer() =
member __.DeserializeAsync<'T> (stream : Stream) = // ...
```

For example, one could define a `Newtonsoft.Json` serializer:
64J0 marked this conversation as resolved.
Show resolved Hide resolved

```fsharp
[<RequireQualifiedAccess>]
module NewtonsoftJson =
open System.IO
open System.Text
open System.Threading.Tasks
open Microsoft.IO
open Newtonsoft.Json
open Newtonsoft.Json.Serialization

type Serializer (settings : JsonSerializerSettings, rmsManager : RecyclableMemoryStreamManager) =
let serializer = JsonSerializer.Create settings
let utf8EncodingWithoutBom = UTF8Encoding(false)

new(settings : JsonSerializerSettings) = Serializer(
settings,
recyclableMemoryStreamManager.Value)

static member DefaultSettings =
JsonSerializerSettings(
ContractResolver = CamelCasePropertyNamesContractResolver())

interface Json.ISerializer with
member __.SerializeToString (x : 'T) =
JsonConvert.SerializeObject(x, settings)

member __.SerializeToBytes (x : 'T) =
JsonConvert.SerializeObject(x, settings)
|> Encoding.UTF8.GetBytes

member __.SerializeToStreamAsync (x : 'T) (stream : Stream) =
task {
use memoryStream = rmsManager.GetStream("giraffe-json-serialize-to-stream")
use streamWriter = new StreamWriter(memoryStream, utf8EncodingWithoutBom)
use jsonTextWriter = new JsonTextWriter(streamWriter)
serializer.Serialize(jsonTextWriter, x)
jsonTextWriter.Flush()
memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore
do! memoryStream.CopyToAsync(stream, 65536)
} :> Task

member __.Deserialize<'T> (json : string) =
JsonConvert.DeserializeObject<'T>(json, settings)

member __.Deserialize<'T> (bytes : byte array) =
let json = Encoding.UTF8.GetString bytes
JsonConvert.DeserializeObject<'T>(json, settings)

member __.DeserializeAsync<'T> (stream : Stream) =
task {
use memoryStream = rmsManager.GetStream("giraffe-json-deserialize")
do! stream.CopyToAsync(memoryStream)
memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore
use streamReader = new StreamReader(memoryStream)
use jsonTextReader = new JsonTextReader(streamReader)
return serializer.Deserialize<'T>(jsonTextReader)
}
```

Then register a new instance of the newly created type during application startup:

```fsharp
Expand All @@ -3012,7 +3021,7 @@ let configureServices (services : IServiceCollection) =
services.AddGiraffe() |> ignore

// Now register your custom Json.ISerializer
services.AddSingleton<Json.ISerializer, CustomJsonSerializer>() |> ignore
services.AddSingleton<Json.ISerializer, NewtonsoftJson.Serializer>() |> ignore

[<EntryPoint>]
let main _ =
Expand All @@ -3025,6 +3034,32 @@ let main _ =
0
```

#### Customizing JsonSerializerSettings

You can change the default `JsonSerializerSettings` of a JSON serializer by registering a new instance of `Json.ISerializer` during application startup. For example, the [`Microsoft.FSharpLu` project](https://github.com/Microsoft/fsharplu/wiki/fsharplu.json) provides a Newtonsoft JSON converter (`CompactUnionJsonConverter`) that serializes and deserializes `Option`s and discriminated unions much more succinctly. If you wanted to use it, and set the culture to German, your configuration would look something like:

```fsharp
let configureServices (services : IServiceCollection) =
// First register all default Giraffe dependencies
services.AddGiraffe() |> ignore
// Now customize only the Json.ISerializer by providing a custom
// object of JsonSerializerSettings
let customSettings = JsonSerializerSettings(
Culture = CultureInfo("de-DE"))
customSettings.Converters.Add(CompactUnionJsonConverter(true))
services.AddSingleton<Json.ISerializer>(
NewtonsoftJson.Serializer(customSettings)) |> ignore
[<EntryPoint>]
let main _ =
WebHost.CreateDefaultBuilder()
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
.Build()
.Run()
0
```

#### Retrieving the JSON serializer from a custom HttpHandler

If you need you retrieve the registered JSON serializer from a custom `HttpHandler` function then you can do this with the `GetJsonSerializer` extension method:
Expand Down
1 change: 0 additions & 1 deletion src/Giraffe/Giraffe.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
<PackageReference Include="FSharp.Core" Version="6.0.0" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.*" />
<PackageReference Include="System.Text.Json" Version="8.0.*" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.*" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.*" PrivateAssets="All" />
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.*" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Giraffe/HttpContextExtensions.fs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ type HttpContextExtensions() =
/// Uses the <see cref="Json.ISerializer"/> to deserialize the entire body of the <see cref="Microsoft.AspNetCore.Http.HttpRequest"/> asynchronously into an object of type 'T.
/// </summary>
/// <typeparam name="'T"></typeparam>
/// <returns>Retruns a <see cref="System.Threading.Tasks.Task{T}"/></returns>
/// <returns>Returns a <see cref="System.Threading.Tasks.Task{T}"/></returns>
64J0 marked this conversation as resolved.
Show resolved Hide resolved
[<Extension>]
static member BindJsonAsync<'T>(ctx : HttpContext) =
task {
Expand Down
75 changes: 4 additions & 71 deletions src/Giraffe/Json.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ namespace Giraffe

[<RequireQualifiedAccess>]
module Json =
open System
open System.IO
open System.Text.Json
open System.Threading.Tasks

/// <summary>
Expand All @@ -19,77 +21,8 @@ module Json =
abstract member Deserialize<'T> : byte[] -> 'T
abstract member DeserializeAsync<'T> : Stream -> Task<'T>

[<RequireQualifiedAccess>]
module NewtonsoftJson =
open System.IO
open System.Text
open System.Threading.Tasks
open Microsoft.IO
open Newtonsoft.Json
open Newtonsoft.Json.Serialization

/// <summary>
/// Default JSON serializer in Giraffe.
/// Serializes objects to camel cased JSON code.
/// </summary>
type Serializer (settings : JsonSerializerSettings, rmsManager : RecyclableMemoryStreamManager) =
let serializer = JsonSerializer.Create settings
let utf8EncodingWithoutBom = UTF8Encoding(false)

new(settings : JsonSerializerSettings) = Serializer(
settings,
recyclableMemoryStreamManager.Value)

static member DefaultSettings =
JsonSerializerSettings(
ContractResolver = CamelCasePropertyNamesContractResolver())

interface Json.ISerializer with
member __.SerializeToString (x : 'T) =
JsonConvert.SerializeObject(x, settings)

member __.SerializeToBytes (x : 'T) =
JsonConvert.SerializeObject(x, settings)
|> Encoding.UTF8.GetBytes

member __.SerializeToStreamAsync (x : 'T) (stream : Stream) =
task {
use memoryStream = rmsManager.GetStream("giraffe-json-serialize-to-stream")
use streamWriter = new StreamWriter(memoryStream, utf8EncodingWithoutBom)
use jsonTextWriter = new JsonTextWriter(streamWriter)
serializer.Serialize(jsonTextWriter, x)
jsonTextWriter.Flush()
memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore
do! memoryStream.CopyToAsync(stream, 65536)
} :> Task

member __.Deserialize<'T> (json : string) =
JsonConvert.DeserializeObject<'T>(json, settings)

member __.Deserialize<'T> (bytes : byte array) =
let json = Encoding.UTF8.GetString bytes
JsonConvert.DeserializeObject<'T>(json, settings)

member __.DeserializeAsync<'T> (stream : Stream) =
task {
use memoryStream = rmsManager.GetStream("giraffe-json-deserialize")
do! stream.CopyToAsync(memoryStream)
memoryStream.Seek(0L, SeekOrigin.Begin) |> ignore
use streamReader = new StreamReader(memoryStream)
use jsonTextReader = new JsonTextReader(streamReader)
return serializer.Deserialize<'T>(jsonTextReader)
}

[<RequireQualifiedAccess>]
module SystemTextJson =
open System
open System.IO
open System.Text
open System.Text.Json
open System.Threading.Tasks

/// <summary>
/// <see cref="SystemTextJson.Serializer" /> is an alternaive <see cref="Json.ISerializer"/> in Giraffe.
/// <see cref="Serializer" /> is the default <see cref="Json.ISerializer"/> in Giraffe.
///
/// It uses <see cref="System.Text.Json"/> as the underlying JSON serializer to (de-)serialize
/// JSON content.
Expand All @@ -103,7 +36,7 @@ module SystemTextJson =
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
)

interface Json.ISerializer with
interface ISerializer with
member __.SerializeToString (x : 'T) =
JsonSerializer.Serialize(x, options)

Expand Down
4 changes: 2 additions & 2 deletions src/Giraffe/Middleware.fs
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ type ServiceCollectionExtensions() =
[<Extension>]
static member AddGiraffe(svc : IServiceCollection) =
svc.TryAddSingleton<RecyclableMemoryStreamManager>(fun _ -> RecyclableMemoryStreamManager())
svc.TryAddSingleton<Json.ISerializer>(fun sp ->
NewtonsoftJson.Serializer(NewtonsoftJson.Serializer.DefaultSettings, sp.GetService<RecyclableMemoryStreamManager>()) :> Json.ISerializer)
svc.TryAddSingleton<Json.ISerializer>(fun _ ->
Json.Serializer(Json.Serializer.DefaultOptions) :> Json.ISerializer)
svc.TryAddSingleton<Xml.ISerializer>(fun sp ->
SystemXml.Serializer(SystemXml.Serializer.DefaultSettings, sp.GetService<RecyclableMemoryStreamManager>()) :> Xml.ISerializer)
svc.TryAddSingleton<INegotiationConfig, DefaultNegotiationConfig>()
Expand Down
47 changes: 6 additions & 41 deletions tests/Giraffe.Tests/Helpers.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ open Microsoft.AspNetCore.Builder
open Microsoft.Extensions.DependencyInjection
open Xunit
open NSubstitute
open System.Text.Json
open Newtonsoft.Json
open Giraffe

// ---------------------------------
Expand Down Expand Up @@ -102,45 +100,12 @@ let createHost (configureApp : 'Tuple -> IApplicationBuilder -> unit)
.Configure(Action<IApplicationBuilder> (configureApp args))
.ConfigureServices(Action<IServiceCollection> configureServices)

type MockJsonSettings =
| Newtonsoft of JsonSerializerSettings option
| SystemTextJson of JsonSerializerOptions option

let mockJson (ctx : HttpContext) (settings : MockJsonSettings) =

match settings with
| Newtonsoft settings ->
let jsonSettings =
defaultArg settings NewtonsoftJson.Serializer.DefaultSettings
ctx.RequestServices
.GetService(typeof<Json.ISerializer>)
.Returns(NewtonsoftJson.Serializer(jsonSettings))
|> ignore

| SystemTextJson settings ->
let jsonOptions =
defaultArg settings SystemTextJson.Serializer.DefaultOptions
ctx.RequestServices
.GetService(typeof<Json.ISerializer>)
.Returns(SystemTextJson.Serializer(jsonOptions))
|> ignore

type JsonSerializersData =

static member DefaultSettings = [
Newtonsoft None
SystemTextJson None
]

static member DefaultData = JsonSerializersData.DefaultSettings |> toTheoryData

static member PreserveCaseSettings =
[
Newtonsoft (Some (JsonSerializerSettings()))
SystemTextJson (Some (JsonSerializerOptions()))
]

static member PreserveCaseData = JsonSerializersData.PreserveCaseSettings |> toTheoryData
let mockJson (ctx : HttpContext) =

ctx.RequestServices
.GetService(typeof<Json.ISerializer>)
.Returns(Json.Serializer(Json.Serializer.DefaultOptions))
|> ignore

type NegotiationConfigWithExpectedResult = {
NegotiationConfig : INegotiationConfig
Expand Down
Loading