Skip to content
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
47 changes: 41 additions & 6 deletions src/FSharp.Data.DesignTime/Json/JsonGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ type internal JsonGenerationContext =
GenerateConstructors: bool
InferenceMode: InferenceMode'
UnitsOfMeasureProvider: IUnitsOfMeasureProvider
UseOriginalNames: bool }
UseOriginalNames: bool
OmitNullFields: bool }

static member Create
(
Expand All @@ -42,7 +43,8 @@ type internal JsonGenerationContext =
?uniqueNiceName,
?typeCache,
?preferDictionaries,
?useOriginalNames
?useOriginalNames,
?omitNullFields
) =
let useOriginalNames = defaultArg useOriginalNames false

Expand All @@ -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,
Expand All @@ -63,7 +66,8 @@ type internal JsonGenerationContext =
true,
inferenceMode,
unitsOfMeasureProvider,
useOriginalNames
useOriginalNames,
omitNullFields
)

static member Create
Expand All @@ -76,7 +80,8 @@ type internal JsonGenerationContext =
generateConstructors,
inferenceMode,
unitsOfMeasureProvider,
useOriginalNames
useOriginalNames,
omitNullFields
) =
{ CultureStr = cultureStr
TypeProviderType = tpType
Expand All @@ -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<option<_>>.MakeGenericType typ
Expand Down Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/FSharp.Data.DesignTime/Json/JsonProvider.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -167,7 +169,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
)
ProvidedStaticParameter("Schema", typeof<string>, parameterDefaultValue = "")
ProvidedStaticParameter("PreferDateOnly", typeof<bool>, parameterDefaultValue = false)
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false) ]
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false)
ProvidedStaticParameter("OmitNullFields", typeof<bool>, parameterDefaultValue = false) ]

let helpText =
"""<summary>Typed representation of a JSON document.</summary>
Expand All @@ -192,7 +195,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
</param>
<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>
<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>
<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>"""
<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>
<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>"""

do jsonProvTy.AddXmlDoc helpText
do jsonProvTy.DefineStaticParameters(parameters, buildTypes)
Expand Down
16 changes: 16 additions & 0 deletions src/FSharp.Data.Json.Core/JsonRuntime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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>)
Expand Down
12 changes: 8 additions & 4 deletions tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ type internal JsonProviderArgs =
InferenceMode: InferenceMode
Schema: string
PreferDateOnly : bool
UseOriginalNames : bool }
UseOriginalNames : bool
OmitNullFields : bool }

type internal HtmlProviderArgs =
{ Sample : string
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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
Expand Down
24 changes: 24 additions & 0 deletions tests/FSharp.Data.Tests/JsonProvider.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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>

[<Test>]
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"

[<Test>]
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"

[<Test>]
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"
Loading