diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 23c7552c8..b69a921b9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # Release Notes +## 8.1.0-beta + +- Add `PreferOptionals` parameter to `JsonProvider` and `XmlProvider` (defaults to `true` to match existing behavior; set to `false` to use empty string or `NaN` for missing values, like the CsvProvider default) (closes #649) + ## 8.0.0 - Feb 25 2026 - Add PreferFloats static parameter to CsvProvider (#1655) diff --git a/src/AssemblyInfo.Csv.Core.fs b/src/AssemblyInfo.Csv.Core.fs index 340e876f7..88d047b19 100644 --- a/src/AssemblyInfo.Csv.Core.fs +++ b/src/AssemblyInfo.Csv.Core.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data.Csv.Core" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/AssemblyInfo.DesignTime.fs b/src/AssemblyInfo.DesignTime.fs index 5b960f5e6..6a0e77278 100644 --- a/src/AssemblyInfo.DesignTime.fs +++ b/src/AssemblyInfo.DesignTime.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data.DesignTime" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/AssemblyInfo.Html.Core.fs b/src/AssemblyInfo.Html.Core.fs index fb1e70bcd..3828c310f 100644 --- a/src/AssemblyInfo.Html.Core.fs +++ b/src/AssemblyInfo.Html.Core.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data.Html.Core" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/AssemblyInfo.Http.fs b/src/AssemblyInfo.Http.fs index 5cbf31d26..c0607906b 100644 --- a/src/AssemblyInfo.Http.fs +++ b/src/AssemblyInfo.Http.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data.Http" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/AssemblyInfo.Json.Core.fs b/src/AssemblyInfo.Json.Core.fs index 1711c441c..182d28a81 100644 --- a/src/AssemblyInfo.Json.Core.fs +++ b/src/AssemblyInfo.Json.Core.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data.Json.Core" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/AssemblyInfo.Runtime.Utilities.fs b/src/AssemblyInfo.Runtime.Utilities.fs index a96b7de15..2507b7785 100644 --- a/src/AssemblyInfo.Runtime.Utilities.fs +++ b/src/AssemblyInfo.Runtime.Utilities.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data.Runtime.Utilities" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/AssemblyInfo.WorldBank.Core.fs b/src/AssemblyInfo.WorldBank.Core.fs index b188e1637..a4c92e260 100644 --- a/src/AssemblyInfo.WorldBank.Core.fs +++ b/src/AssemblyInfo.WorldBank.Core.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data.WorldBank.Core" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/AssemblyInfo.Xml.Core.fs b/src/AssemblyInfo.Xml.Core.fs index dcaf811e6..986e467f8 100644 --- a/src/AssemblyInfo.Xml.Core.fs +++ b/src/AssemblyInfo.Xml.Core.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data.Xml.Core" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/AssemblyInfo.fs b/src/AssemblyInfo.fs index f962f7f78..5ce024ff6 100644 --- a/src/AssemblyInfo.fs +++ b/src/AssemblyInfo.fs @@ -5,13 +5,13 @@ open System.Reflection [] [] [] -[] -[] +[] +[] do () module internal AssemblyVersionInformation = let [] AssemblyTitle = "FSharp.Data" let [] AssemblyProduct = "FSharp.Data" let [] AssemblyDescription = "Library of F# type providers and data access tools" - let [] AssemblyVersion = "8.0.0.0" - let [] AssemblyFileVersion = "8.0.0.0" + let [] AssemblyVersion = "8.1.0.0" + let [] AssemblyFileVersion = "8.1.0.0" diff --git a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs index 0969bbe62..fbd2688a0 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs @@ -60,6 +60,7 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = let preferDateOnly = args.[11] :?> bool let useOriginalNames = args.[12] :?> bool let omitNullFields = args.[13] :?> bool + let preferOptionals = args.[14] :?> bool let inferenceMode = InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues) @@ -100,8 +101,14 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = samples |> Array.map (fun sampleJson -> - JsonInference.inferType unitsOfMeasureProvider inferenceMode cultureInfo "" sampleJson) - |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top + JsonInference.inferType + unitsOfMeasureProvider + inferenceMode + cultureInfo + (not preferOptionals) + "" + sampleJson) + |> Array.fold (StructuralInference.subtypeInfered (not preferOptionals)) InferedType.Top #if NET6_0_OR_GREATER if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then rawInfered @@ -170,7 +177,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("Schema", typeof, parameterDefaultValue = "") ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) ProvidedStaticParameter("UseOriginalNames", typeof, parameterDefaultValue = false) - ProvidedStaticParameter("OmitNullFields", typeof, parameterDefaultValue = false) ] + ProvidedStaticParameter("OmitNullFields", typeof, parameterDefaultValue = false) + ProvidedStaticParameter("PreferOptionals", typeof, parameterDefaultValue = true) ] let helpText = """Typed representation of a JSON document. @@ -196,7 +204,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, optional fields with value None are omitted from the generated JSON rather than serialized as null. Defaults to false.""" + When true, optional fields with value None are omitted from the generated JSON rather than serialized as null. Defaults to false. + When set to true (default), inference will use the option type for missing or null values. When false, inference will prefer to use empty string or double.NaN for missing values where possible, matching the default CsvProvider behavior.""" do jsonProvTy.AddXmlDoc helpText do jsonProvTy.DefineStaticParameters(parameters, buildTypes) diff --git a/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs b/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs index c6604a7dc..bb7b9cdbf 100644 --- a/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs +++ b/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs @@ -53,6 +53,7 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = let preferDateOnly = args.[10] :?> bool let dtdProcessing = args.[11] :?> string let useOriginalNames = args.[12] :?> bool + let preferOptionals = args.[13] :?> bool let inferenceMode = InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues) @@ -133,9 +134,9 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = unitsOfMeasureProvider inferenceMode (TextRuntime.GetCulture cultureStr) - false + (not preferOptionals) globalInference - |> Array.fold (StructuralInference.subtypeInfered false) InferedType.Top + |> Array.fold (StructuralInference.subtypeInfered (not preferOptionals)) InferedType.Top #if NET6_0_OR_GREATER if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then t @@ -203,7 +204,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = ) ProvidedStaticParameter("PreferDateOnly", typeof, parameterDefaultValue = false) ProvidedStaticParameter("DtdProcessing", typeof, parameterDefaultValue = "Ignore") - ProvidedStaticParameter("UseOriginalNames", typeof, parameterDefaultValue = false) ] + ProvidedStaticParameter("UseOriginalNames", typeof, parameterDefaultValue = false) + ProvidedStaticParameter("PreferOptionals", typeof, parameterDefaultValue = true) ] let helpText = """Typed representation of a XML file. @@ -229,7 +231,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = When true on .NET 6+, date-only strings are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility. Controls how DTD declarations in the XML are handled. Accepted values: "Ignore" (default, silently skips DTD processing, safe for most cases), "Prohibit" (throws on any DTD declaration), "Parse" (enables full DTD processing including entity expansion, use with caution). - When true, XML element and attribute names are used as-is for generated property names instead of being normalized to PascalCase. Defaults to false.""" + When true, XML element and attribute names are used as-is for generated property names instead of being normalized to PascalCase. Defaults to false. + When set to true (default), inference will use the option type for missing or absent values. When false, inference will prefer to use empty string or double.NaN for missing values where possible, matching the default CsvProvider behavior.""" do xmlProvTy.AddXmlDoc helpText diff --git a/src/FSharp.Data.Json.Core/JsonInference.fs b/src/FSharp.Data.Json.Core/JsonInference.fs index 1699e66b9..0d80b636f 100644 --- a/src/FSharp.Data.Json.Core/JsonInference.fs +++ b/src/FSharp.Data.Json.Core/JsonInference.fs @@ -14,7 +14,7 @@ open FSharp.Data.Runtime.StructuralInference /// functionality is handled in `StructureInference` (most notably, by /// `inferCollectionType` and various functions to find common subtype), so /// here we just need to infer types of primitive JSON values. -let rec internal inferType unitsOfMeasureProvider inferenceMode cultureInfo parentName json = +let rec internal inferType unitsOfMeasureProvider inferenceMode cultureInfo allowEmptyValues parentName json = let inline inRangeDecimal lo hi (v: decimal) : bool = (v >= decimal lo) && (v <= decimal hi) let inline inRangeFloat lo hi (v: float) : bool = (v >= float lo) && (v <= float hi) let inline isIntegerDecimal (v: decimal) : bool = Math.Round v = v @@ -65,8 +65,15 @@ let rec internal inferType unitsOfMeasureProvider inferenceMode cultureInfo pare // More interesting types | JsonValue.Array ar -> StructuralInference.inferCollectionType - false - (Seq.map (inferType unitsOfMeasureProvider inferenceMode cultureInfo (NameUtils.singularize parentName)) ar) + allowEmptyValues + (Seq.map + (inferType + unitsOfMeasureProvider + inferenceMode + cultureInfo + allowEmptyValues + (NameUtils.singularize parentName)) + ar) | JsonValue.Record properties -> let name = if String.IsNullOrEmpty parentName then @@ -76,7 +83,9 @@ let rec internal inferType unitsOfMeasureProvider inferenceMode cultureInfo pare let props = [ for propName, value in properties -> - let t = inferType unitsOfMeasureProvider inferenceMode cultureInfo propName value + let t = + inferType unitsOfMeasureProvider inferenceMode cultureInfo allowEmptyValues propName value + { Name = propName; Type = t } ] InferedType.Record(name, props, false) diff --git a/src/FSharp.Data.Xml.Core/XmlInference.fs b/src/FSharp.Data.Xml.Core/XmlInference.fs index f2cb6c3aa..b4374a563 100644 --- a/src/FSharp.Data.Xml.Core/XmlInference.fs +++ b/src/FSharp.Data.Xml.Core/XmlInference.fs @@ -56,6 +56,7 @@ let getInferedTypeFromValue unitsOfMeasureProvider inferenceMode cultureInfo (el unitsOfMeasureProvider inferenceMode cultureInfo + false element.Name.LocalName InferedType.Json(jsonType, optional) diff --git a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs index cdbdd4132..d97e6461e 100644 --- a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs +++ b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs @@ -49,14 +49,14 @@ let ``List.pairBy helper function preserves order``() = let ``Finds common subtype of numeric types (decimal)``() = let source = JsonValue.Parse """[ 10, 10.23 ]""" let expected = SimpleCollection(InferedType.Primitive(typeof, None, false, false)) - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] let ``Finds common subtype of numeric types (int64)``() = let source = JsonValue.Parse """[ 10, 2147483648 ]""" let expected = SimpleCollection(InferedType.Primitive(typeof, None, false, false)) - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -67,7 +67,7 @@ let ``Infers heterogeneous type of InferedType.Primitives``() = ([ InferedTypeTag.Number; InferedTypeTag.Boolean ], [ InferedTypeTag.Number, (Single, InferedType.Primitive(typeof, None, false, false)) InferedTypeTag.Boolean, (Single, InferedType.Primitive(typeof, None, false, false)) ] |> Map.ofList) - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -79,14 +79,14 @@ let ``Infers heterogeneous type of InferedType.Primitives and nulls``() = [ InferedTypeTag.Null, (Single, InferedType.Null) InferedTypeTag.Number, (Single, InferedType.Primitive(typeof, None, false, false)) InferedTypeTag.Boolean, (Single, InferedType.Primitive(typeof, None, false, false)) ] |> Map.ofList) - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] let ``Finds common subtype of numeric types (float)``() = let source = JsonValue.Parse """[ 10, 10.23, 79228162514264337593543950336 ]""" let expected = SimpleCollection(InferedType.Primitive(typeof, None, false, false)) - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -98,7 +98,7 @@ let ``Infers heterogeneous type of InferedType.Primitives and records``() = [ InferedTypeTag.Number, (Multiple, InferedType.Primitive(typeof, None, false, false)) InferedTypeTag.Record None, (Single, toRecord [ { Name="a"; Type=InferedType.Primitive(typeof, None, false, false) } ]) ] |> Map.ofList) - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -111,7 +111,7 @@ let ``Merges types in a collection of collections``() = |> toRecord |> SimpleCollection |> SimpleCollection - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -123,7 +123,7 @@ let ``Unions properties of records in a collection``() = { Name = "c"; Type = InferedType.Primitive(typeof, None, true, false) } ] |> toRecord |> SimpleCollection - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -133,7 +133,7 @@ let ``Null should make string optional``() = [ { Name = "a"; Type = InferedType.Primitive(typeof, None, true, false) } ] |> toRecord |> SimpleCollection - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -150,7 +150,7 @@ let ``Infers mixed fields of a a record as heterogeneous type with nulls (1.)``( [ { Name = "a"; Type = InferedType.Primitive(typeof, None, true, false) } ] |> toRecord |> SimpleCollection - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -160,7 +160,7 @@ let ``Null makes a record optional``() = [ { Name = "a"; Type = InferedType.Record(Some "a", [{ Name = "b"; Type = InferedType.Primitive(typeof, None, false, false) }], true) } ] |> toRecord |> SimpleCollection - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -173,7 +173,7 @@ let ``Infers mixed fields of a record as heterogeneous type``() = [ { Name = "a"; Type = InferedType.Heterogeneous (cases, false) }] |> toRecord |> SimpleCollection - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -183,7 +183,7 @@ let ``Infers mixed fields of a record as heterogeneous type with nulls (2.)``() [ { Name = "a"; Type = InferedType.Primitive(typeof, None, true, false) }] |> toRecord |> SimpleCollection - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -195,7 +195,7 @@ let ``Inference of multiple nulls works``() = ([ InferedTypeTag.Number; InferedTypeTag.Collection ], [ InferedTypeTag.Collection, (Single, SimpleCollection(toRecord [prop])) InferedTypeTag.Number, (Single, InferedType.Primitive(typeof, None, false, false)) ] |> Map.ofList) - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] @@ -406,7 +406,7 @@ let ``Doesn't infer 12-002 as a date``() = ([ InferedTypeTag.String; InferedTypeTag.Number], [ InferedTypeTag.String, (Multiple, InferedType.Primitive(typeof, None, false, false)) InferedTypeTag.Number, (Single, InferedType.Primitive(typeof, None, false, false)) ] |> Map.ofList) - let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture false "" source actual |> should equal expected [] diff --git a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs index bd060f25b..2e004b464 100644 --- a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs +++ b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs @@ -44,7 +44,8 @@ type internal XmlProviderArgs = InferenceMode: InferenceMode PreferDateOnly : bool DtdProcessing : string - UseOriginalNames : bool } + UseOriginalNames : bool + PreferOptionals : bool } type internal JsonProviderArgs = { Sample : string @@ -60,7 +61,8 @@ type internal JsonProviderArgs = Schema: string PreferDateOnly : bool UseOriginalNames : bool - OmitNullFields : bool } + OmitNullFields : bool + PreferOptionals : bool } type internal HtmlProviderArgs = { Sample : string @@ -123,7 +125,8 @@ type internal TypeProviderInstantiation = box x.InferenceMode box x.PreferDateOnly box x.DtdProcessing - box x.UseOriginalNames |] + box x.UseOriginalNames + box x.PreferOptionals |] | Json x -> (fun cfg -> new JsonProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -139,7 +142,8 @@ type internal TypeProviderInstantiation = box x.Schema box x.PreferDateOnly box x.UseOriginalNames - box x.OmitNullFields |] + box x.OmitNullFields + box x.PreferOptionals |] | Html x -> (fun cfg -> new HtmlProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -263,7 +267,8 @@ type internal TypeProviderInstantiation = InferenceMode = args.[7] |> InferenceMode.Parse PreferDateOnly = false DtdProcessing = "Ignore" - UseOriginalNames = false } + UseOriginalNames = false + PreferOptionals = true } | "Json" -> // Handle special case for Schema.json tests where some fields might be empty if args.Length > 5 && not (String.IsNullOrEmpty(args.[5])) then @@ -280,7 +285,8 @@ type internal TypeProviderInstantiation = Schema = if args.Length > 8 then args.[8] else "" PreferDateOnly = false UseOriginalNames = false - OmitNullFields = false } + OmitNullFields = false + PreferOptionals = true } else // This is for schema-based tests in the format "Json,,,,,true,false,BackwardCompatible,SimpleSchema.json" Json { Sample = args.[1] @@ -296,7 +302,8 @@ type internal TypeProviderInstantiation = Schema = if args.Length > 8 then args.[8] else "" PreferDateOnly = false UseOriginalNames = false - OmitNullFields = false } + OmitNullFields = false + PreferOptionals = true } | "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 c6b455665..21afc3d2e 100644 --- a/tests/FSharp.Data.Tests/JsonProvider.fs +++ b/tests/FSharp.Data.Tests/JsonProvider.fs @@ -1019,3 +1019,23 @@ let ``JsonProvider OmitNullFields=true includes non-None fields`` () = let json = value.ToString() json |> should contain "42" json |> should contain "Blue" + +// Tests for PreferOptionals parameter on JsonProvider (issue #649) +type JsonPreferOptionalsFalse = + JsonProvider<"""[{"name": "Alice", "tag": "x"}, {"name": "Bob"}]""", SampleIsList = true, PreferOptionals = false> + +type JsonPreferOptionalsTrue = + JsonProvider<"""[{"name": "Alice", "tag": "x"}, {"name": "Bob"}]""", SampleIsList = true, PreferOptionals = true> + +[] +let ``JsonProvider PreferOptionals=true (default) uses option type for missing string fields`` () = + let doc = JsonPreferOptionalsTrue.Parse("""{"name": "Alice", "tag": "x"}""") + doc.Tag.GetType() |> should equal typeof + let docMissing = JsonPreferOptionalsTrue.Parse("""{"name": "Bob"}""") + docMissing.Tag |> should equal None + +[] +let ``JsonProvider PreferOptionals=false uses empty string for missing string fields`` () = + let doc = JsonPreferOptionalsFalse.Parse("""{"name": "Bob"}""") + doc.Tag.GetType() |> should equal typeof + doc.Tag |> should equal "" diff --git a/tests/FSharp.Data.Tests/XmlProvider.fs b/tests/FSharp.Data.Tests/XmlProvider.fs index ed5574276..2c551eeba 100644 --- a/tests/FSharp.Data.Tests/XmlProvider.fs +++ b/tests/FSharp.Data.Tests/XmlProvider.fs @@ -1340,3 +1340,22 @@ let ``XmlProvider UseOriginalNames=true preserves attribute names as-is`` () = let ``XmlProvider default normalizes attribute names to PascalCase`` () = let root = XmlNormalizedNames.Parse("") root.FaultCode |> should equal "world" + +// Tests for PreferOptionals parameter on XmlProvider (issue #649) +[] +let xmlOptionalAttr = """""" + +type XmlPreferOptionalsFalse = XmlProvider +type XmlPreferOptionalsTrue = XmlProvider + +[] +let ``XmlProvider PreferOptionals=true (default) uses option type for optional attributes`` () = + let root = XmlPreferOptionalsTrue.Parse("""""") + root.Items.[0].Tag.GetType() |> should equal typeof + root.Items.[1].Tag |> should equal None + +[] +let ``XmlProvider PreferOptionals=false uses empty string for missing string attributes`` () = + let root = XmlPreferOptionalsFalse.Parse("""""") + root.Items.[0].Tag.GetType() |> should equal typeof + root.Items.[0].Tag |> should equal ""