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

Add F# compatible json serializer #609

Merged
merged 5 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
10 changes: 5 additions & 5 deletions DOCUMENTATION.md
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also second way - just use the recyclableMemoryStreamManager constant:

    new(settings : JsonSerializerSettings) = Serializer(
        settings,
        Lazy<RecyclableMemoryStreamManager>().Value)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know if this other approach does provide some improvement compared to the one mentioned at this PR? Just curiosity due to this usage of Lazy<'T> and RecyclableMemoryStreamManager.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe not important one, but as Lazy<RecyclableMemoryStreamManager>() was the default, it was one argument less in code. :)

Just started using Giraffe and OpenApi and you guys are doing great work with those 🎉 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, thanks for the feedback 🚀

Original file line number Diff line number Diff line change
Expand Up @@ -2935,6 +2935,9 @@ Please visit the [Giraffe.ViewEngine](https://github.com/giraffe-fsharp/Giraffe.

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.

It's possible to use a serializer compatible with Fsharp types: `Json.FsharpFriendlySerializer` instead of `Json.Serializer`.
This uses `FSharp.SystemTextJson` to customize `System.Text.Json`.

#### Using a different JSON serializer

You can change the entire underlying JSON serializer by creating a new class which implements the `Json.ISerializer` interface:
Expand Down Expand Up @@ -2968,10 +2971,6 @@ For example, one could define a `Newtonsoft.Json` serializer:
let serializer = JsonSerializer.Create settings
let utf8EncodingWithoutBom = UTF8Encoding(false)

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

static member DefaultSettings =
JsonSerializerSettings(
ContractResolver = CamelCasePropertyNamesContractResolver())
Expand Down Expand Up @@ -3021,7 +3020,8 @@ let configureServices (services : IServiceCollection) =
services.AddGiraffe() |> ignore

// Now register your custom Json.ISerializer
services.AddSingleton<Json.ISerializer, NewtonsoftJson.Serializer>() |> ignore
services.AddSingleton<Json.ISerializer>(fun serviceProvider ->
NewtonsoftJson.Serializer(JsonSerializerSettings(), serviceProvider.GetService<Microsoft.IO.RecyclableMemoryStreamManager>()) :> Json.ISerializer) |> ignore

[<EntryPoint>]
let main _ =
Expand Down
1 change: 1 addition & 0 deletions src/Giraffe/Giraffe.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
<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="FSharp.SystemTextJson" Version="1.3.*" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.*" PrivateAssets="All" />
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.*" />
</ItemGroup>
Expand Down
19 changes: 18 additions & 1 deletion src/Giraffe/Json.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Json =
open System.IO
open System.Text.Json
open System.Threading.Tasks
open System.Text.Json.Serialization

/// <summary>
/// Interface defining JSON serialization methods.
Expand Down Expand Up @@ -53,4 +54,20 @@ module Json =
JsonSerializer.Deserialize<'T>(Span<_>.op_Implicit(bytes.AsSpan()), options)

member __.DeserializeAsync<'T> (stream : Stream) : Task<'T> =
JsonSerializer.DeserializeAsync<'T>(stream, options).AsTask()
JsonSerializer.DeserializeAsync<'T>(stream, options).AsTask()

module FsharpFriendlySerializer =
let DefaultOptions =
JsonFSharpOptions.Default()

let private appendJsonFSharpOptions (fsharpOptions: JsonFSharpOptions) (jsonOptions: JsonSerializerOptions) =
jsonOptions.Converters.Add(JsonFSharpConverter(fsharpOptions))
jsonOptions

let buildConfig (fsharpOptions: JsonFSharpOptions option) (jsonOptions: JsonSerializerOptions option) =
jsonOptions
|> Option.defaultValue (JsonSerializerOptions())
|> appendJsonFSharpOptions (fsharpOptions |> Option.defaultValue DefaultOptions)

type FsharpFriendlySerializer (?fsharpOptions: JsonFSharpOptions, ?jsonOptions: JsonSerializerOptions) =
inherit Serializer(FsharpFriendlySerializer.buildConfig fsharpOptions jsonOptions)
70 changes: 70 additions & 0 deletions tests/Giraffe.Tests/HttpHandlerTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,76 @@ let ``GET "/json" returns json object`` () =
| Some ctx -> Assert.Equal(expected, getBody ctx)
}

type ResponseWithFsharpType = {
ValueA: string option
ValueB: JsonUnionCaseDummy
}
and JsonUnionCaseDummy =
| JsonUnionCaseDummyA of int
| JsonUnionCaseDummyB
[<Fact>]
let ``GET "/json" returns json object with fsharp type`` () =
let ctx = Substitute.For<HttpContext>()
ctx.RequestServices
.GetService(typeof<Json.ISerializer>)
.Returns(Json.FsharpFriendlySerializer())
|> ignore

let app =
GET >=> choose [
route "/" >=> text "Hello World"
route "/foo" >=> text "bar"
route "/json" >=> json { ValueA = Some "hello"; ValueB = JsonUnionCaseDummyA 42 }
setStatusCode 404 >=> text "Not found" ]

ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore
ctx.Request.Path.ReturnsForAnyArgs (PathString("/json")) |> ignore
ctx.Response.Body <- new MemoryStream()
let expected = """{"ValueA":"hello","ValueB":{"Case":"JsonUnionCaseDummyA","Fields":[42]}}"""

task {
let! result = app next ctx

match result with
| None -> assertFailf "Result was expected to be %s" expected
| Some ctx ->
let content = getBody ctx
Assert.Equal(expected, content)
}

[<Fact>]
let ``GET "/json" returns json object with fsharp type and use custom config`` () =
let ctx = Substitute.For<HttpContext>()
let customConfig =
System.Text.Json.Serialization.JsonFSharpOptions.Default()
.WithUnionTagNamingPolicy(System.Text.Json.JsonNamingPolicy.CamelCase)
ctx.RequestServices
.GetService(typeof<Json.ISerializer>)
.Returns(Json.FsharpFriendlySerializer(customConfig, Json.Serializer.DefaultOptions))
|> ignore

let app =
GET >=> choose [
route "/" >=> text "Hello World"
route "/foo" >=> text "bar"
route "/json" >=> json { ValueA = Some "hello"; ValueB = JsonUnionCaseDummyA 42 }
setStatusCode 404 >=> text "Not found" ]

ctx.Request.Method.ReturnsForAnyArgs "GET" |> ignore
ctx.Request.Path.ReturnsForAnyArgs (PathString("/json")) |> ignore
ctx.Response.Body <- new MemoryStream()
let expected = """{"valueA":"hello","valueB":{"Case":"jsonUnionCaseDummyA","Fields":[42]}}"""

task {
let! result = app next ctx

match result with
| None -> assertFailf "Result was expected to be %s" expected
| Some ctx ->
let content = getBody ctx
Assert.Equal(expected, content)
}

let DefaultMocksWithSize =
[
let ``powers of two`` = [ 1..10 ] |> List.map (pown 2)
Expand Down
Loading