Skip to content

Commit 3cfefcf

Browse files
Copilotxperiandri
andcommitted
Address review: add JsonSerializerOptions to context/request, use JsonSerializer.Serialize for variables
Agent-Logs-Url: https://github.com/fsprojects/FSharp.Data.GraphQL/sessions/63700bd7-a033-4fb4-a394-21e09ca3943e Co-authored-by: xperiandri <2365592+xperiandri@users.noreply.github.com>
1 parent f1bdef9 commit 3cfefcf

5 files changed

Lines changed: 47 additions & 62 deletions

File tree

src/FSharp.Data.GraphQL.Client.DesignTime/ProvidedTypesHelper.fs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ open System.Collections
88
open System.Collections.Generic
99
open System.Net.Http
1010
open System.Reflection
11+
open System.Text.Json
1112
open System.Text.Json.Serialization
1213
open FSharp.Core
1314
open FSharp.Data.GraphQL
@@ -333,7 +334,7 @@ module internal ProvidedOperation =
333334
let serverUrl = info.ServerUrl
334335
let headerNames = info.HttpHeaders |> Seq.map fst |> Array.ofSeq
335336
let headerValues = info.HttpHeaders |> Seq.map snd |> Array.ofSeq
336-
<@@ { ServerUrl = serverUrl; HttpHeaders = Array.zip headerNames headerValues; Connection = new GraphQLClientConnection() } @@>
337+
<@@ { ServerUrl = serverUrl; HttpHeaders = Array.zip headerNames headerValues; Connection = new GraphQLClientConnection(); JsonSerializerOptions = Serialization.defaultSerializerOptions.Value } @@>
337338
| None -> <@@ Unchecked.defaultof<GraphQLProviderRuntimeContext> @@>
338339
// We need to use the combination strategy to generate overloads for variables in the Run/AsyncRun methods.
339340
// The strategy follows the same principle with ProvidedRecord constructor overloads,
@@ -403,7 +404,8 @@ module internal ProvidedOperation =
403404
HttpHeaders = context.HttpHeaders
404405
OperationName = Option.ofObj operationName
405406
Query = actualQuery
406-
Variables = %%variables }
407+
Variables = %%variables
408+
JsonSerializerOptions = context.JsonSerializerOptions }
407409
let response =
408410
if shouldUseMultipartRequest
409411
then Tracer.runAndMeasureExecutionTime "Ran a multipart GraphQL query request" (fun _ -> GraphQLClient.sendMultipartRequest context.Connection request)
@@ -448,7 +450,8 @@ module internal ProvidedOperation =
448450
HttpHeaders = context.HttpHeaders
449451
OperationName = Option.ofObj operationName
450452
Query = actualQuery
451-
Variables = %%variables }
453+
Variables = %%variables
454+
JsonSerializerOptions = context.JsonSerializerOptions }
452455
async {
453456
let! ct = Async.CancellationToken
454457
let! response =
@@ -751,7 +754,8 @@ module internal Provider =
751754
| _ -> ProvidedParameter("serverUrl", typeof<string>)
752755
let httpHeaders = ProvidedParameter("httpHeaders", typeof<seq<string * string>>, optionalValue = null)
753756
let connectionFactory = ProvidedParameter("connectionFactory", typeof<unit -> GraphQLClientConnection>, optionalValue = null)
754-
[serverUrl; httpHeaders; connectionFactory]
757+
let jsonSerializerOptions = ProvidedParameter("jsonSerializerOptions", typeof<JsonSerializerOptions>, optionalValue = null)
758+
[serverUrl; httpHeaders; connectionFactory; jsonSerializerOptions]
755759
let defaultHttpHeadersExpr =
756760
let names = httpHeaders |> Seq.map fst |> Array.ofSeq
757761
let values = httpHeaders |> Seq.map snd |> Array.ofSeq
@@ -766,7 +770,11 @@ module internal Provider =
766770
match %%args.[2] : unit -> GraphQLClientConnection with
767771
| argHeaders when obj.Equals(argHeaders, null) -> fun () -> new GraphQLClientConnection()
768772
| argHeaders -> argHeaders
769-
{ ServerUrl = %%serverUrl; HttpHeaders = httpHeaders; Connection = connectionFactory() } @@>
773+
let jsonOptions =
774+
match %%args.[3] : JsonSerializerOptions with
775+
| null -> Serialization.defaultSerializerOptions.Value
776+
| opts -> opts
777+
{ ServerUrl = %%serverUrl; HttpHeaders = httpHeaders; Connection = connectionFactory(); JsonSerializerOptions = jsonOptions } @@>
770778
ProvidedMethod("GetContext", methodParameters, typeof<GraphQLProviderRuntimeContext>, invoker, isStatic = true)
771779
let operationMethodDef =
772780
let staticParams =

src/FSharp.Data.GraphQL.Client/BaseTypes.fs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ module VariableMapping =
636636
| :? string -> value
637637
| :? EnumBase as v -> v.GetValue () |> box
638638
| :? RecordBase as v -> v.ToDictionary () |> box
639-
| OptionValue v -> v |> Option.map mapVariableValue |> box
639+
| OptionValue None -> null
640+
| OptionValue (Some v) -> mapVariableValue v
640641
| EnumerableValue v -> v |> Array.map mapVariableValue |> box
641642
| v -> v

src/FSharp.Data.GraphQL.Client/GraphQLClient.fs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ open System
77
open System.Collections.Generic
88
open System.Net.Http
99
open System.Text
10+
open System.Text.Json
1011
open System.Threading
1112
open System.Threading.Tasks
1213

@@ -26,6 +27,8 @@ type GraphQLRequest = {
2627
Query : string
2728
/// Gets variables to be sent with the query.
2829
Variables : (string * obj)[]
30+
/// Gets the JSON serializer options used for serializing request variables.
31+
JsonSerializerOptions : JsonSerializerOptions
2932
}
3033

3134
/// Executes calls to GraphQL servers and return their responses.
@@ -56,7 +59,7 @@ module GraphQLClient =
5659
/// Sends a request to a GraphQL server asynchronously.
5760
let sendRequestAsync ct (connection : GraphQLClientConnection) (request : GraphQLRequest) = task {
5861
let invoker = connection.Invoker
59-
let json = Serialization.buildRequestJson request.OperationName request.Query request.Variables
62+
let json = Serialization.buildRequestJson request.JsonSerializerOptions request.OperationName request.Query request.Variables
6063
let content = new StringContent (json, Encoding.UTF8, "application/json")
6164
return! postAsync ct invoker request.ServerUrl request.HttpHeaders content
6265
}
@@ -88,6 +91,7 @@ module GraphQLClient =
8891
OperationName = None
8992
Query = IntrospectionQuery.Definition
9093
Variables = [||]
94+
JsonSerializerOptions = Serialization.defaultSerializerOptions.Value
9195
}
9296
try
9397
return! sendRequestAsync ct connection request
@@ -130,7 +134,7 @@ module GraphQLClient =
130134
|> Array.collect (tryMapFileVariable >> (Option.defaultValue [||]))
131135

132136
let operationContent =
133-
let json = Serialization.buildRequestJson request.OperationName request.Query request.Variables
137+
let json = Serialization.buildRequestJson request.JsonSerializerOptions request.OperationName request.Query request.Variables
134138
let content = new StringContent (json)
135139
content.Headers.Add ("Content-Disposition", "form-data; name=\"operations\"")
136140
content

src/FSharp.Data.GraphQL.Client/GraphQLProviderRuntimeContext.fs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
namespace FSharp.Data.GraphQL
55

66
open System
7+
open System.Text.Json
78

89
/// Contains information about a GraphQLRuntimeContext.
910
type GraphQLRuntimeContextInfo =
@@ -17,6 +18,9 @@ type GraphQLProviderRuntimeContext =
1718
/// Gets the HTTP headers used for calls to the server that this context refers to.
1819
HttpHeaders : seq<string * string>
1920
/// Gets the connection component used to make calls to the server.
20-
Connection : GraphQLClientConnection }
21+
Connection : GraphQLClientConnection
22+
/// Gets the JSON serializer options used for serializing request variables and
23+
/// deserializing scalar values. Pass a customized instance to support custom scalar types.
24+
JsonSerializerOptions : JsonSerializerOptions }
2125
interface IDisposable with
2226
member x.Dispose() = (x.Connection :> IDisposable).Dispose()

src/FSharp.Data.GraphQL.Client/Serialization.fs

Lines changed: 21 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ namespace FSharp.Data.GraphQL.Client
66
open System
77
open System.Collections.Generic
88
open System.IO
9-
open System.Reflection
109
open System.Text
1110
open System.Text.Json
1211
open FSharp.Data.GraphQL
@@ -202,8 +201,24 @@ module private SchemaParser =
202201

203202
module Serialization =
204203

205-
let private isoDateFormat = "yyyy-MM-dd"
206-
let private isoDateTimeFormat = "O"
204+
/// The default JSON serializer options used for request serialization when no custom options are provided.
205+
let defaultSerializerOptions =
206+
lazy (FSharp.Data.GraphQL.Shared.Json.getSerializerOptions Seq.empty)
207+
208+
/// Converts special types (Uri, Upload, etc.) that System.Text.Json cannot handle natively
209+
/// into their JSON-serializable representations. Applied recursively to variable values.
210+
/// Also normalizes dictionary keys to camelCase to match GraphQL field naming conventions.
211+
let rec private normalizeForSerialization (value : obj) : obj =
212+
match value with
213+
| null -> null
214+
| :? string -> value // Must come before EnumerableValue: string implements IEnumerable
215+
| :? Uri as u -> box (u.ToString ())
216+
| :? Upload as u -> box u.Name // File variables are written as the form-part name string
217+
| :? IDictionary<string, obj> as d ->
218+
// Apply FirstCharLower to keys: RecordBase.ToDictionary() uses PascalCase (FirstCharUpper) for property names
219+
d |> Seq.map (fun kvp -> kvp.Key.FirstCharLower (), normalizeForSerialization kvp.Value) |> dict |> box
220+
| EnumerableValue items -> items |> Array.map normalizeForSerialization |> box
221+
| v -> v
207222

208223
/// Converts a JsonElement to an F# object recursively.
209224
let rec private deserializeElement (element : JsonElement) : obj =
@@ -236,55 +251,11 @@ module Serialization =
236251
|> Array.map (fun (name, element) -> name, deserializeElement element)
237252
|> Map.ofArray)
238253

239-
let private writeValue (writer : Utf8JsonWriter) =
240-
let rec write (value : obj) =
241-
match value with
242-
| null -> writer.WriteNullValue ()
243-
| OptionValue None -> writer.WriteNullValue ()
244-
| OptionValue (Some v) -> write v
245-
| :? bool as b -> writer.WriteBooleanValue b
246-
| :? int as n -> writer.WriteNumberValue n
247-
| :? float as f -> writer.WriteNumberValue f
248-
| :? decimal as d -> writer.WriteNumberValue d
249-
| :? int64 as n -> writer.WriteNumberValue n
250-
| :? uint64 as n -> writer.WriteNumberValue n
251-
| :? int16 as n -> writer.WriteNumberValue (int n)
252-
| :? uint16 as n -> writer.WriteNumberValue (uint32 n)
253-
| :? byte as n -> writer.WriteNumberValue (uint32 n)
254-
| :? sbyte as n -> writer.WriteNumberValue (int n)
255-
| :? string as s -> writer.WriteStringValue s
256-
| :? Guid as g -> writer.WriteStringValue (g.ToString ())
257-
| :? DateTime as d when d.Date = d -> writer.WriteStringValue (d.ToString isoDateFormat)
258-
| :? DateTime as d -> writer.WriteStringValue (d.ToString isoDateTimeFormat)
259-
| :? DateTimeOffset as d -> writer.WriteStringValue (d.ToString isoDateTimeFormat)
260-
| :? Uri as u -> writer.WriteStringValue (u.ToString ())
261-
| :? Upload as u -> writer.WriteStringValue u.Name
262-
| :? IDictionary<string, obj> as dict ->
263-
writer.WriteStartObject ()
264-
for kvp in dict do
265-
writer.WritePropertyName (kvp.Key.FirstCharLower ())
266-
write kvp.Value
267-
writer.WriteEndObject ()
268-
| EnumerableValue items ->
269-
writer.WriteStartArray ()
270-
Array.iter write items
271-
writer.WriteEndArray ()
272-
| EnumValue s -> writer.WriteStringValue s
273-
| _ ->
274-
let props = value.GetType().GetProperties (BindingFlags.Public ||| BindingFlags.Instance)
275-
writer.WriteStartObject ()
276-
for p in props do
277-
writer.WritePropertyName (p.Name.FirstCharLower ())
278-
write (p.GetValue value)
279-
writer.WriteEndObject ()
280-
write
281-
282254
/// Builds the JSON body for a standard GraphQL request.
283-
let buildRequestJson (operationName : string option) (query : string) (variables : (string * obj) []) =
255+
let buildRequestJson (options : JsonSerializerOptions) (operationName : string option) (query : string) (variables : (string * obj) []) =
284256
Tracer.runAndMeasureExecutionTime "Built GraphQL request JSON" (fun _ ->
285257
use stream = new MemoryStream ()
286258
use writer = new Utf8JsonWriter (stream, JsonWriterOptions (Indented = false))
287-
let write = writeValue writer
288259
writer.WriteStartObject ()
289260
writer.WritePropertyName "operationName"
290261
match operationName with
@@ -296,11 +267,8 @@ module Serialization =
296267
if variables = null || variables.Length = 0 then
297268
writer.WriteNullValue ()
298269
else
299-
writer.WriteStartObject ()
300-
for (name, value) in variables do
301-
writer.WritePropertyName name
302-
write value
303-
writer.WriteEndObject ()
270+
let dict = variables |> Array.map (fun (k, v) -> k, normalizeForSerialization v) |> dict
271+
JsonSerializer.Serialize (writer, dict, options)
304272
writer.WriteEndObject ()
305273
writer.Flush ()
306274
Encoding.UTF8.GetString (stream.ToArray ()))

0 commit comments

Comments
 (0)