Skip to content

Commit d6599df

Browse files
[Repo Assist] Add OmitNullFields static parameter to JsonProvider (closes #1245) (#1638)
* Add OmitNullFields static parameter to JsonProvider When OmitNullFields=true, optional fields with value None are omitted from the generated JSON rather than serialized as null. This addresses the request in issue #1245: users integrating with strict APIs that reject null fields can now use: type ColorProvider = JsonProvider<"...", SampleIsList=true, OmitNullFields=true> let value = ColorProvider.Root(color = "Blue", code = None) // Serializes as: {"color": "Blue"} (no "code": null) Changes: - JsonRuntime.fs: Add CreateRecordOmitNulls() that filters null fields - JsonGenerator.fs: Add OmitNullFields field to JsonGenerationContext; use CreateRecordOmitNulls when OmitNullFields=true - JsonProvider.fs: Add OmitNullFields static parameter (default false) - TypeProviderInstantiation.fs: Update test helper with new param - JsonProvider.fs (tests): Add 3 tests covering new behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger CI checks --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 483317f commit d6599df

5 files changed

Lines changed: 96 additions & 13 deletions

File tree

src/FSharp.Data.DesignTime/Json/JsonGenerator.fs

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ type internal JsonGenerationContext =
3131
GenerateConstructors: bool
3232
InferenceMode: InferenceMode'
3333
UnitsOfMeasureProvider: IUnitsOfMeasureProvider
34-
UseOriginalNames: bool }
34+
UseOriginalNames: bool
35+
OmitNullFields: bool }
3536

3637
static member Create
3738
(
@@ -42,7 +43,8 @@ type internal JsonGenerationContext =
4243
?uniqueNiceName,
4344
?typeCache,
4445
?preferDictionaries,
45-
?useOriginalNames
46+
?useOriginalNames,
47+
?omitNullFields
4648
) =
4749
let useOriginalNames = defaultArg useOriginalNames false
4850

@@ -53,6 +55,7 @@ type internal JsonGenerationContext =
5355

5456
let typeCache = defaultArg typeCache (Dictionary())
5557
let preferDictionaries = defaultArg preferDictionaries false
58+
let omitNullFields = defaultArg omitNullFields false
5659

5760
JsonGenerationContext.Create(
5861
cultureStr,
@@ -63,7 +66,8 @@ type internal JsonGenerationContext =
6366
true,
6467
inferenceMode,
6568
unitsOfMeasureProvider,
66-
useOriginalNames
69+
useOriginalNames,
70+
omitNullFields
6771
)
6872

6973
static member Create
@@ -76,7 +80,8 @@ type internal JsonGenerationContext =
7680
generateConstructors,
7781
inferenceMode,
7882
unitsOfMeasureProvider,
79-
useOriginalNames
83+
useOriginalNames,
84+
omitNullFields
8085
) =
8186
{ CultureStr = cultureStr
8287
TypeProviderType = tpType
@@ -89,7 +94,33 @@ type internal JsonGenerationContext =
8994
GenerateConstructors = generateConstructors
9095
InferenceMode = inferenceMode
9196
UnitsOfMeasureProvider = unitsOfMeasureProvider
92-
UseOriginalNames = useOriginalNames }
97+
UseOriginalNames = useOriginalNames
98+
OmitNullFields = omitNullFields }
99+
100+
static member Create
101+
(
102+
cultureStr,
103+
tpType,
104+
uniqueNiceName,
105+
typeCache,
106+
preferDictionaries,
107+
generateConstructors,
108+
inferenceMode,
109+
unitsOfMeasureProvider,
110+
useOriginalNames
111+
) =
112+
JsonGenerationContext.Create(
113+
cultureStr,
114+
tpType,
115+
uniqueNiceName,
116+
typeCache,
117+
preferDictionaries,
118+
generateConstructors,
119+
inferenceMode,
120+
unitsOfMeasureProvider,
121+
useOriginalNames,
122+
false
123+
)
93124

94125
member x.MakeOptionType(typ: Type) =
95126
typedefof<option<_>>.MakeGenericType typ
@@ -645,7 +676,11 @@ module JsonTypeBuilder =
645676
)
646677

647678
let cultureStr = ctx.CultureStr
648-
<@@ JsonRuntime.CreateRecord(%%properties, cultureStr) @@>
679+
680+
if ctx.OmitNullFields then
681+
<@@ JsonRuntime.CreateRecordOmitNulls(%%properties, cultureStr) @@>
682+
else
683+
<@@ JsonRuntime.CreateRecord(%%properties, cultureStr) @@>
649684

650685
let ctor = ProvidedConstructor(parameters, invokeCode = ctorCode)
651686
objectTy.AddMember ctor

src/FSharp.Data.DesignTime/Json/JsonProvider.fs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
5959
let schema = args.[10] :?> string
6060
let preferDateOnly = args.[11] :?> bool
6161
let useOriginalNames = args.[12] :?> bool
62+
let omitNullFields = args.[13] :?> bool
6263

6364
let inferenceMode =
6465
InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues)
@@ -119,7 +120,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
119120
unitsOfMeasureProvider,
120121
inferenceMode,
121122
?preferDictionaries = Some preferDictionaries,
122-
?useOriginalNames = Some useOriginalNames
123+
?useOriginalNames = Some useOriginalNames,
124+
?omitNullFields = Some omitNullFields
123125
)
124126

125127
let result = JsonTypeBuilder.generateJsonType ctx false false rootName inferedType
@@ -167,7 +169,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
167169
)
168170
ProvidedStaticParameter("Schema", typeof<string>, parameterDefaultValue = "")
169171
ProvidedStaticParameter("PreferDateOnly", typeof<bool>, parameterDefaultValue = false)
170-
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false) ]
172+
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false)
173+
ProvidedStaticParameter("OmitNullFields", typeof<bool>, parameterDefaultValue = false) ]
171174

172175
let helpText =
173176
"""<summary>Typed representation of a JSON document.</summary>
@@ -192,7 +195,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
192195
</param>
193196
<param name='Schema'>Location of a JSON Schema file or a string containing a JSON Schema document. When specified, Sample and SampleIsList must not be used.</param>
194197
<param name='PreferDateOnly'>When true on .NET 6+, date-only strings (e.g. "2023-01-15") are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility.</param>
195-
<param name='UseOriginalNames'>When true, JSON property names are used as-is for generated property names instead of being normalized to PascalCase. Defaults to false.</param>"""
198+
<param name='UseOriginalNames'>When true, JSON property names are used as-is for generated property names instead of being normalized to PascalCase. Defaults to false.</param>
199+
<param name='OmitNullFields'>When true, optional fields with value None are omitted from the generated JSON rather than serialized as null. Defaults to false.</param>"""
196200

197201
do jsonProvTy.AddXmlDoc helpText
198202
do jsonProvTy.DefineStaticParameters(parameters, buildTypes)

src/FSharp.Data.Json.Core/JsonRuntime.fs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,22 @@ type JsonRuntime =
351351

352352
JsonDocument.Create(json, "")
353353

354+
// Creates a JsonValue.Record, omitting null fields, and wraps it in a json document
355+
static member CreateRecordOmitNulls(properties, cultureStr) =
356+
let cultureInfo = TextRuntime.GetCulture cultureStr
357+
358+
let json =
359+
properties
360+
|> Array.choose (fun (k, v: obj) ->
361+
let jv = JsonRuntime.ToJsonValue cultureInfo v
362+
363+
match jv with
364+
| JsonValue.Null -> None
365+
| _ -> Some(k, jv))
366+
|> JsonValue.Record
367+
368+
JsonDocument.Create(json, "")
369+
354370
// Creates a JsonValue.Record from key*value seq and wraps it in a json document
355371
static member CreateRecordFromDictionary<'Key, 'Value when 'Key: equality>
356372
(keyValuePairs: ('Key * 'Value) seq, cultureStr, mappingKeyBack: Func<'Key, string>)

tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ type internal JsonProviderArgs =
5757
InferenceMode: InferenceMode
5858
Schema: string
5959
PreferDateOnly : bool
60-
UseOriginalNames : bool }
60+
UseOriginalNames : bool
61+
OmitNullFields : bool }
6162

6263
type internal HtmlProviderArgs =
6364
{ Sample : string
@@ -133,7 +134,8 @@ type internal TypeProviderInstantiation =
133134
box x.InferenceMode
134135
box x.Schema
135136
box x.PreferDateOnly
136-
box x.UseOriginalNames |]
137+
box x.UseOriginalNames
138+
box x.OmitNullFields |]
137139
| Html x ->
138140
(fun cfg -> new HtmlProvider(cfg) :> TypeProviderForNamespaces),
139141
[| box x.Sample
@@ -271,7 +273,8 @@ type internal TypeProviderInstantiation =
271273
InferenceMode = args.[7] |> InferenceMode.Parse
272274
Schema = if args.Length > 8 then args.[8] else ""
273275
PreferDateOnly = false
274-
UseOriginalNames = false }
276+
UseOriginalNames = false
277+
OmitNullFields = false }
275278
else
276279
// This is for schema-based tests in the format "Json,,,,,true,false,BackwardCompatible,SimpleSchema.json"
277280
Json { Sample = args.[1]
@@ -286,7 +289,8 @@ type internal TypeProviderInstantiation =
286289
InferenceMode = InferenceMode.Parse "BackwardCompatible"
287290
Schema = if args.Length > 8 then args.[8] else ""
288291
PreferDateOnly = false
289-
UseOriginalNames = false }
292+
UseOriginalNames = false
293+
OmitNullFields = false }
290294
| "Html" ->
291295
Html { Sample = args.[1]
292296
PreferOptionals = args.[2] |> bool.Parse

tests/FSharp.Data.Tests/JsonProvider.fs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -968,3 +968,27 @@ let ``JsonProvider default normalizes property names to PascalCase`` () =
968968
let doc = JsonNormalizedNames.Parse("""{"first_name": "Jane", "last_name": "Smith"}""")
969969
doc.FirstName |> should equal "Jane"
970970
doc.LastName |> should equal "Smith"
971+
972+
type JsonOmitNullFieldsSample = JsonProvider<"""[{"color": "Red", "code": 15}, {"color": "Green"}]""", SampleIsList = true, OmitNullFields = true>
973+
type JsonIncludeNullFieldsSample = JsonProvider<"""[{"color": "Red", "code": 15}, {"color": "Green"}]""", SampleIsList = true>
974+
975+
[<Test>]
976+
let ``JsonProvider OmitNullFields=true omits None optional fields from output`` () =
977+
let value = JsonOmitNullFieldsSample.Root(color = "Blue", code = None)
978+
let json = value.ToString()
979+
json |> should not' (contain "null")
980+
json |> should not' (contain "code")
981+
json |> should contain "Blue"
982+
983+
[<Test>]
984+
let ``JsonProvider default includes None optional fields as null`` () =
985+
let value = JsonIncludeNullFieldsSample.Root(color = "Blue", code = None)
986+
let json = value.ToString()
987+
json |> should contain "null"
988+
989+
[<Test>]
990+
let ``JsonProvider OmitNullFields=true includes non-None fields`` () =
991+
let value = JsonOmitNullFieldsSample.Root(color = "Blue", code = Some 42)
992+
let json = value.ToString()
993+
json |> should contain "42"
994+
json |> should contain "Blue"

0 commit comments

Comments
 (0)