diff --git a/src/FSharp.Data.DesignTime/Json/JsonGenerator.fs b/src/FSharp.Data.DesignTime/Json/JsonGenerator.fs index 336141c02..b011208ca 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonGenerator.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonGenerator.fs @@ -31,7 +31,8 @@ type internal JsonGenerationContext = GenerateConstructors: bool InferenceMode: InferenceMode' UnitsOfMeasureProvider: IUnitsOfMeasureProvider - UseOriginalNames: bool } + UseOriginalNames: bool + OmitNullFields: bool } static member Create ( @@ -42,7 +43,8 @@ type internal JsonGenerationContext = ?uniqueNiceName, ?typeCache, ?preferDictionaries, - ?useOriginalNames + ?useOriginalNames, + ?omitNullFields ) = let useOriginalNames = defaultArg useOriginalNames false @@ -53,6 +55,7 @@ type internal JsonGenerationContext = let typeCache = defaultArg typeCache (Dictionary()) let preferDictionaries = defaultArg preferDictionaries false + let omitNullFields = defaultArg omitNullFields false JsonGenerationContext.Create( cultureStr, @@ -63,7 +66,8 @@ type internal JsonGenerationContext = true, inferenceMode, unitsOfMeasureProvider, - useOriginalNames + useOriginalNames, + omitNullFields ) static member Create @@ -76,7 +80,8 @@ type internal JsonGenerationContext = generateConstructors, inferenceMode, unitsOfMeasureProvider, - useOriginalNames + useOriginalNames, + omitNullFields ) = { CultureStr = cultureStr TypeProviderType = tpType @@ -89,7 +94,33 @@ type internal JsonGenerationContext = GenerateConstructors = generateConstructors InferenceMode = inferenceMode UnitsOfMeasureProvider = unitsOfMeasureProvider - UseOriginalNames = useOriginalNames } + UseOriginalNames = useOriginalNames + OmitNullFields = omitNullFields } + + static member Create + ( + cultureStr, + tpType, + uniqueNiceName, + typeCache, + preferDictionaries, + generateConstructors, + inferenceMode, + unitsOfMeasureProvider, + useOriginalNames + ) = + JsonGenerationContext.Create( + cultureStr, + tpType, + uniqueNiceName, + typeCache, + preferDictionaries, + generateConstructors, + inferenceMode, + unitsOfMeasureProvider, + useOriginalNames, + false + ) member x.MakeOptionType(typ: Type) = typedefof>.MakeGenericType typ @@ -645,7 +676,11 @@ module JsonTypeBuilder = ) let cultureStr = ctx.CultureStr - <@@ JsonRuntime.CreateRecord(%%properties, cultureStr) @@> + + if ctx.OmitNullFields then + <@@ JsonRuntime.CreateRecordOmitNulls(%%properties, cultureStr) @@> + else + <@@ JsonRuntime.CreateRecord(%%properties, cultureStr) @@> let ctor = ProvidedConstructor(parameters, invokeCode = ctorCode) objectTy.AddMember ctor diff --git a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs index fb2fae7cd..0969bbe62 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs @@ -59,6 +59,7 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = let schema = args.[10] :?> string let preferDateOnly = args.[11] :?> bool let useOriginalNames = args.[12] :?> bool + let omitNullFields = args.[13] :?> bool let inferenceMode = InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues) @@ -119,7 +120,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = unitsOfMeasureProvider, inferenceMode, ?preferDictionaries = Some preferDictionaries, - ?useOriginalNames = Some useOriginalNames + ?useOriginalNames = Some useOriginalNames, + ?omitNullFields = Some omitNullFields ) let result = JsonTypeBuilder.generateJsonType ctx false false rootName inferedType @@ -167,7 +169,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = ) ProvidedStaticParameter("Schema", typeof, parameterDefaultValue = "") ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) - ProvidedStaticParameter("UseOriginalNames", typeof, parameterDefaultValue = false) ] + ProvidedStaticParameter("UseOriginalNames", typeof, parameterDefaultValue = false) + ProvidedStaticParameter("OmitNullFields", typeof, parameterDefaultValue = false) ] let helpText = """Typed representation of a JSON document. @@ -192,7 +195,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = Location of a JSON Schema file or a string containing a JSON Schema document. When specified, Sample and SampleIsList must not be used. 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. - When true, JSON property names are used as-is for generated property names instead of being normalized to PascalCase. Defaults to false.""" + When true, JSON property names are used as-is for generated property names instead of being normalized to PascalCase. Defaults to false. + When true, optional fields with value None are omitted from the generated JSON rather than serialized as null. Defaults to false.""" do jsonProvTy.AddXmlDoc helpText do jsonProvTy.DefineStaticParameters(parameters, buildTypes) diff --git a/src/FSharp.Data.Json.Core/JsonRuntime.fs b/src/FSharp.Data.Json.Core/JsonRuntime.fs index c2c2931a5..378238b7f 100644 --- a/src/FSharp.Data.Json.Core/JsonRuntime.fs +++ b/src/FSharp.Data.Json.Core/JsonRuntime.fs @@ -351,6 +351,22 @@ type JsonRuntime = JsonDocument.Create(json, "") + // Creates a JsonValue.Record, omitting null fields, and wraps it in a json document + static member CreateRecordOmitNulls(properties, cultureStr) = + let cultureInfo = TextRuntime.GetCulture cultureStr + + let json = + properties + |> Array.choose (fun (k, v: obj) -> + let jv = JsonRuntime.ToJsonValue cultureInfo v + + match jv with + | JsonValue.Null -> None + | _ -> Some(k, jv)) + |> JsonValue.Record + + JsonDocument.Create(json, "") + // Creates a JsonValue.Record from key*value seq and wraps it in a json document static member CreateRecordFromDictionary<'Key, 'Value when 'Key: equality> (keyValuePairs: ('Key * 'Value) seq, cultureStr, mappingKeyBack: Func<'Key, string>) diff --git a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs index 8c6d25572..500b18d9c 100644 --- a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs +++ b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs @@ -57,7 +57,8 @@ type internal JsonProviderArgs = InferenceMode: InferenceMode Schema: string PreferDateOnly : bool - UseOriginalNames : bool } + UseOriginalNames : bool + OmitNullFields : bool } type internal HtmlProviderArgs = { Sample : string @@ -133,7 +134,8 @@ type internal TypeProviderInstantiation = box x.InferenceMode box x.Schema box x.PreferDateOnly - box x.UseOriginalNames |] + box x.UseOriginalNames + box x.OmitNullFields |] | Html x -> (fun cfg -> new HtmlProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -271,7 +273,8 @@ type internal TypeProviderInstantiation = InferenceMode = args.[7] |> InferenceMode.Parse Schema = if args.Length > 8 then args.[8] else "" PreferDateOnly = false - UseOriginalNames = false } + UseOriginalNames = false + OmitNullFields = false } else // This is for schema-based tests in the format "Json,,,,,true,false,BackwardCompatible,SimpleSchema.json" Json { Sample = args.[1] @@ -286,7 +289,8 @@ type internal TypeProviderInstantiation = InferenceMode = InferenceMode.Parse "BackwardCompatible" Schema = if args.Length > 8 then args.[8] else "" PreferDateOnly = false - UseOriginalNames = false } + UseOriginalNames = false + OmitNullFields = false } | "Html" -> Html { Sample = args.[1] PreferOptionals = args.[2] |> bool.Parse diff --git a/tests/FSharp.Data.Tests/JsonProvider.fs b/tests/FSharp.Data.Tests/JsonProvider.fs index 9dd317733..5cb8585c4 100644 --- a/tests/FSharp.Data.Tests/JsonProvider.fs +++ b/tests/FSharp.Data.Tests/JsonProvider.fs @@ -968,3 +968,27 @@ let ``JsonProvider default normalizes property names to PascalCase`` () = let doc = JsonNormalizedNames.Parse("""{"first_name": "Jane", "last_name": "Smith"}""") doc.FirstName |> should equal "Jane" doc.LastName |> should equal "Smith" + +type JsonOmitNullFieldsSample = JsonProvider<"""[{"color": "Red", "code": 15}, {"color": "Green"}]""", SampleIsList = true, OmitNullFields = true> +type JsonIncludeNullFieldsSample = JsonProvider<"""[{"color": "Red", "code": 15}, {"color": "Green"}]""", SampleIsList = true> + +[] +let ``JsonProvider OmitNullFields=true omits None optional fields from output`` () = + let value = JsonOmitNullFieldsSample.Root(color = "Blue", code = None) + let json = value.ToString() + json |> should not' (contain "null") + json |> should not' (contain "code") + json |> should contain "Blue" + +[] +let ``JsonProvider default includes None optional fields as null`` () = + let value = JsonIncludeNullFieldsSample.Root(color = "Blue", code = None) + let json = value.ToString() + json |> should contain "null" + +[] +let ``JsonProvider OmitNullFields=true includes non-None fields`` () = + let value = JsonOmitNullFieldsSample.Root(color = "Blue", code = Some 42) + let json = value.ToString() + json |> should contain "42" + json |> should contain "Blue"