From af8dd126599b7178cc3cdeb7bbf6c71c75eeeaf1 Mon Sep 17 00:00:00 2001 From: Repo Assist Date: Sat, 28 Feb 2026 12:28:55 +0000 Subject: [PATCH 1/2] Add ExceptionIfMissing static parameter to JsonProvider and XmlProvider Implements opt-in strict mode for missing field handling. When ExceptionIfMissing=true, accessing a non-optional field that is absent in the data raises an exception instead of silently returning a default value (empty string for string, NaN for float). The default behavior (ExceptionIfMissing=false) is unchanged for backward compatibility. - Added GetNonOptionalValueStrict<'T> to JsonRuntime and TextRuntime - Added ExceptionIfMissing static parameter to JsonProvider and XmlProvider - Updated code generation pipeline (JsonConversionsGenerator, ConversionsGenerator) - CsvProvider and HtmlProvider always use non-strict mode (false) - Updated TypeProviderInstantiation.fs test helper - Added tests for ExceptionIfMissing behavior Also fixes a pre-existing XmlProvider bug where args.[14] was used for both UseSchemaTypeNames and PreferDateTimeOffset (should be args.[14] and args.[15]). Closes #1241 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 1 + .../ConversionsGenerator.fs | 12 ++++--- .../Csv/CsvGenerator.fs | 2 +- .../Html/HtmlGenerator.fs | 6 +++- .../Json/JsonConversionsGenerator.fs | 11 +++++-- .../Json/JsonGenerator.fs | 19 +++++++---- .../Json/JsonProvider.fs | 10 ++++-- .../Xml/XmlGenerator.fs | 19 +++++++---- src/FSharp.Data.DesignTime/Xml/XmlProvider.fs | 15 ++++++--- src/FSharp.Data.Json.Core/JsonRuntime.fs | 21 ++++++++++++ .../TextRuntime.fs | 8 +++++ .../TypeProviderInstantiation.fs | 21 ++++++++---- tests/FSharp.Data.Tests/JsonProvider.fs | 33 +++++++++++++++++++ tests/FSharp.Data.Tests/XmlProvider.fs | 16 +++++++++ 14 files changed, 159 insertions(+), 35 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 334a65517..b11f90d87 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,7 @@ ## 8.1.0-beta +- Add `ExceptionIfMissing` static parameter to `JsonProvider` and `XmlProvider`: when true, accessing a non-optional field that is missing in the data raises an exception instead of silently returning a default value (empty string for string, NaN for float). Defaults to false for backward compatibility. - Add `Http.ParseLinkHeader` utility for parsing RFC 5988 `Link` response headers (used by GitHub, GitLab, and other paginated APIs) into a `Map` from relation name to URL (closes #805) - Add `PreferDateTimeOffset` parameter to `CsvProvider`, `JsonProvider`, and `XmlProvider`: when true, date-time values without an explicit timezone offset are inferred as `DateTimeOffset` (using local offset) instead of `DateTime` (closes #1100, #1072) - Make `Http.AppendQueryToUrl` public (closes #1325) diff --git a/src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs b/src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs index 007350767..eb04096aa 100644 --- a/src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs +++ b/src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs @@ -75,7 +75,7 @@ let getBackConversionQuotation missingValuesStr cultureStr typ value : Expr and converts it to /// an expression of other type - the type is specified by `field` -let internal convertStringValue missingValuesStr cultureStr (field: PrimitiveInferedProperty) = +let internal convertStringValue missingValuesStr cultureStr exceptionIfMissing (field: PrimitiveInferedProperty) = let fieldName = field.Name let field = field.Value @@ -95,6 +95,12 @@ let internal convertStringValue missingValuesStr cultureStr (field: PrimitiveInf let convert value = getConversionQuotation missingValuesStr cultureStr field.InferedType value + let getNonOptionalValueMethod = + if exceptionIfMissing then + nameof (TextRuntime.GetNonOptionalValueStrict) + else + nameof (TextRuntime.GetNonOptionalValue) + match field.TypeWrapper with | TypeWrapper.None -> //prevent value being calculated twice @@ -102,9 +108,7 @@ let internal convertStringValue missingValuesStr cultureStr (field: PrimitiveInf let varExpr = Expr.Cast(Expr.Var var) let body = - typeof?(nameof (TextRuntime.GetNonOptionalValue)) - field.RuntimeType - (fieldName, convert varExpr, varExpr) + typeof?(getNonOptionalValueMethod) field.RuntimeType (fieldName, convert varExpr, varExpr) Expr.Let(var, value, body) | TypeWrapper.Option -> convert value diff --git a/src/FSharp.Data.DesignTime/Csv/CsvGenerator.fs b/src/FSharp.Data.DesignTime/Csv/CsvGenerator.fs index f50b9dbef..2fa0456b8 100644 --- a/src/FSharp.Data.DesignTime/Csv/CsvGenerator.fs +++ b/src/FSharp.Data.DesignTime/Csv/CsvGenerator.fs @@ -33,7 +33,7 @@ module internal CsvTypeBuilder = inferredFields |> List.mapi (fun index field -> let typ, typWithoutMeasure, conv, convBack = - ConversionsGenerator.convertStringValue missingValuesStr cultureStr field + ConversionsGenerator.convertStringValue missingValuesStr cultureStr false field let propertyName = if useOriginalNames then diff --git a/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs b/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs index f3362b694..2d84cf4dc 100644 --- a/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs +++ b/src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs @@ -67,7 +67,7 @@ module internal HtmlGenerator = columns |> List.mapi (fun index field -> let typ, typWithoutMeasure, conv, _convBack = - ConversionsGenerator.convertStringValue missingValuesStr cultureStr field + ConversionsGenerator.convertStringValue missingValuesStr cultureStr false field { TypeForTuple = typWithoutMeasure ProvidedProperty = @@ -165,6 +165,7 @@ module internal HtmlGenerator = ConversionsGenerator.convertStringValue missingValuesStr cultureStr + false (StructuralTypes.PrimitiveInferedProperty.Create("", typ, optional, None)) typ, conv @@ -173,6 +174,7 @@ module internal HtmlGenerator = ConversionsGenerator.convertStringValue missingValuesStr cultureStr + false (StructuralTypes.PrimitiveInferedProperty.Create("", typeof, false, None)) typ, conv @@ -238,6 +240,7 @@ module internal HtmlGenerator = ConversionsGenerator.convertStringValue missingValuesStr cultureStr + false (StructuralTypes.PrimitiveInferedProperty.Create("", typ, optional, None)) typ, conv @@ -246,6 +249,7 @@ module internal HtmlGenerator = ConversionsGenerator.convertStringValue missingValuesStr cultureStr + false (StructuralTypes.PrimitiveInferedProperty.Create("", typeof, false, None)) typ, conv diff --git a/src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs b/src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs index a6e0f3480..289405e63 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonConversionsGenerator.fs @@ -56,6 +56,7 @@ let internal convertJsonValue missingValuesStr cultureStr canPassAllConversionCallingTypes + exceptionIfMissing (field: PrimitiveInferedValue) = @@ -80,15 +81,21 @@ let internal convertJsonValue let convert value = getConversionQuotation missingValuesStr cultureStr field.InferedType value + let getNonOptionalValueMethod = + if exceptionIfMissing then + nameof (JsonRuntime.GetNonOptionalValueStrict) + else + nameof (JsonRuntime.GetNonOptionalValue) + match field.TypeWrapper, canPassAllConversionCallingTypes with | TypeWrapper.None, true -> wrapInLetIfNeeded value (fun (varExpr: Expr) -> - typeof?(nameof (JsonRuntime.GetNonOptionalValue)) + typeof?(getNonOptionalValueMethod) (field.RuntimeType) (<@ (%varExpr).Path @>, convert <@ (%varExpr).JsonOpt @>, <@ (%varExpr).JsonOpt @>)) | TypeWrapper.None, false -> wrapInLetIfNeeded value (fun (varExpr: Expr) -> - typeof?(nameof (JsonRuntime.GetNonOptionalValue)) + typeof?(getNonOptionalValueMethod) (field.RuntimeType) (<@ (%varExpr).Path() @>, convert <@ Some (%varExpr).JsonValue @>, <@ Some (%varExpr).JsonValue @>)) | TypeWrapper.Option, true -> convert <@ (%%value: JsonValue option) @> diff --git a/src/FSharp.Data.DesignTime/Json/JsonGenerator.fs b/src/FSharp.Data.DesignTime/Json/JsonGenerator.fs index f00d33f76..19218c3a5 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonGenerator.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonGenerator.fs @@ -32,7 +32,8 @@ type internal JsonGenerationContext = InferenceMode: InferenceMode' UnitsOfMeasureProvider: IUnitsOfMeasureProvider UseOriginalNames: bool - OmitNullFields: bool } + OmitNullFields: bool + ExceptionIfMissing: bool } static member Create ( @@ -44,7 +45,8 @@ type internal JsonGenerationContext = ?typeCache, ?preferDictionaries, ?useOriginalNames, - ?omitNullFields + ?omitNullFields, + ?exceptionIfMissing ) = let useOriginalNames = defaultArg useOriginalNames false @@ -56,6 +58,7 @@ type internal JsonGenerationContext = let typeCache = defaultArg typeCache (Dictionary()) let preferDictionaries = defaultArg preferDictionaries false let omitNullFields = defaultArg omitNullFields false + let exceptionIfMissing = defaultArg exceptionIfMissing false JsonGenerationContext.Create( cultureStr, @@ -67,7 +70,8 @@ type internal JsonGenerationContext = inferenceMode, unitsOfMeasureProvider, useOriginalNames, - omitNullFields + omitNullFields, + exceptionIfMissing ) static member Create @@ -81,7 +85,8 @@ type internal JsonGenerationContext = inferenceMode, unitsOfMeasureProvider, useOriginalNames, - omitNullFields + omitNullFields, + exceptionIfMissing ) = { CultureStr = cultureStr TypeProviderType = tpType @@ -95,7 +100,8 @@ type internal JsonGenerationContext = InferenceMode = inferenceMode UnitsOfMeasureProvider = unitsOfMeasureProvider UseOriginalNames = useOriginalNames - OmitNullFields = omitNullFields } + OmitNullFields = omitNullFields + ExceptionIfMissing = exceptionIfMissing } static member Create ( @@ -119,6 +125,7 @@ type internal JsonGenerationContext = inferenceMode, unitsOfMeasureProvider, useOriginalNames, + false, false ) @@ -367,7 +374,7 @@ module JsonTypeBuilder = let typ, conv, conversionCallingType = PrimitiveInferedValue.Create(inferedType, optional, unit) - |> convertJsonValue "" ctx.CultureStr canPassAllConversionCallingTypes + |> convertJsonValue "" ctx.CultureStr canPassAllConversionCallingTypes ctx.ExceptionIfMissing { ConvertedType = typ OptionalConverter = Some conv diff --git a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs index a9c290c97..5c46790b2 100644 --- a/src/FSharp.Data.DesignTime/Json/JsonProvider.fs +++ b/src/FSharp.Data.DesignTime/Json/JsonProvider.fs @@ -62,6 +62,7 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = let omitNullFields = args.[13] :?> bool let preferOptionals = args.[14] :?> bool let preferDateTimeOffset = args.[15] :?> bool + let exceptionIfMissing = args.[16] :?> bool let inferenceMode = InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues) @@ -136,7 +137,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = inferenceMode, ?preferDictionaries = Some preferDictionaries, ?useOriginalNames = Some useOriginalNames, - ?omitNullFields = Some omitNullFields + ?omitNullFields = Some omitNullFields, + ?exceptionIfMissing = Some exceptionIfMissing ) let result = JsonTypeBuilder.generateJsonType ctx false false rootName inferedType @@ -187,7 +189,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("UseOriginalNames", typeof, parameterDefaultValue = false) ProvidedStaticParameter("OmitNullFields", typeof, parameterDefaultValue = false) ProvidedStaticParameter("PreferOptionals", typeof, parameterDefaultValue = true) - ProvidedStaticParameter("PreferDateTimeOffset", typeof, parameterDefaultValue = false) ] + ProvidedStaticParameter("PreferDateTimeOffset", typeof, parameterDefaultValue = false) + ProvidedStaticParameter("ExceptionIfMissing", typeof, parameterDefaultValue = false) ] let helpText = """Typed representation of a JSON document. @@ -215,7 +218,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this = 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 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. - When true, date-time strings without an explicit timezone offset are inferred as DateTimeOffset (using the local offset) instead of DateTime. Defaults to false.""" + When true, date-time strings without an explicit timezone offset are inferred as DateTimeOffset (using the local offset) instead of DateTime. Defaults to false. + When true, accessing a non-optional field that is missing in the JSON data raises an exception instead of returning a default value (empty string for string, NaN for float). Defaults to false for backward compatibility.""" do jsonProvTy.AddXmlDoc helpText do jsonProvTy.DefineStaticParameters(parameters, buildTypes) diff --git a/src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs b/src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs index 60e4ec74b..ec65cc822 100644 --- a/src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs +++ b/src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs @@ -30,12 +30,15 @@ type internal XmlGenerationContext = UnifyGlobally: bool XmlTypeCache: Dictionary JsonTypeCache: Dictionary - UseOriginalNames: bool } + UseOriginalNames: bool + ExceptionIfMissing: bool } - static member Create(unitsOfMeasureProvider, inferenceMode, cultureStr, tpType, unifyGlobally, useOriginalNames) = + static member Create + (unitsOfMeasureProvider, inferenceMode, cultureStr, tpType, unifyGlobally, useOriginalNames, ?exceptionIfMissing) = let niceName = if useOriginalNames then id else NameUtils.nicePascalName let uniqueNiceName = NameUtils.uniqueGenerator niceName uniqueNiceName "XElement" |> ignore + let exceptionIfMissing = defaultArg exceptionIfMissing false { CultureStr = cultureStr UnitsOfMeasureProvider = unitsOfMeasureProvider @@ -45,15 +48,18 @@ type internal XmlGenerationContext = UnifyGlobally = unifyGlobally XmlTypeCache = Dictionary() JsonTypeCache = Dictionary() - UseOriginalNames = useOriginalNames } + UseOriginalNames = useOriginalNames + ExceptionIfMissing = exceptionIfMissing } member x.ConvertValue prop = - let typ, _, conv, _ = ConversionsGenerator.convertStringValue "" x.CultureStr prop + let typ, _, conv, _ = + ConversionsGenerator.convertStringValue "" x.CultureStr x.ExceptionIfMissing prop + typ, conv member x.ConvertValueBack prop = let typ, _, _, convBack = - ConversionsGenerator.convertStringValue "" x.CultureStr prop + ConversionsGenerator.convertStringValue "" x.CultureStr x.ExceptionIfMissing prop typ, convBack @@ -155,7 +161,8 @@ module internal XmlTypeBuilder = ctx.UnitsOfMeasureProvider, ctx.InferenceMode, ctx.UniqueNiceName, - ctx.JsonTypeCache + ctx.JsonTypeCache, + ?exceptionIfMissing = Some ctx.ExceptionIfMissing ) let result = JsonTypeBuilder.generateJsonType ctx false true "" typ diff --git a/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs b/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs index 08a2f9939..58d65196b 100644 --- a/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs +++ b/src/FSharp.Data.DesignTime/Xml/XmlProvider.fs @@ -55,7 +55,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = let useOriginalNames = args.[12] :?> bool let preferOptionals = args.[13] :?> bool let useSchemaTypeNames = args.[14] :?> bool - let preferDateTimeOffset = args.[14] :?> bool + let preferDateTimeOffset = args.[15] :?> bool + let exceptionIfMissing = args.[16] :?> bool let inferenceMode = InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues) @@ -110,7 +111,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = cultureStr, tpType, globalInference || schema <> "", - useOriginalNames + useOriginalNames, + exceptionIfMissing ) let result = XmlTypeBuilder.generateXmlType ctx inferedType @@ -174,7 +176,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = cultureStr, tpType, globalInference || schema <> "", - useOriginalNames + useOriginalNames, + exceptionIfMissing ) let result = XmlTypeBuilder.generateXmlType ctx inferedType @@ -226,7 +229,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = ProvidedStaticParameter("UseOriginalNames", typeof, parameterDefaultValue = false) ProvidedStaticParameter("PreferOptionals", typeof, parameterDefaultValue = true) ProvidedStaticParameter("UseSchemaTypeNames", typeof, parameterDefaultValue = false) - ProvidedStaticParameter("PreferDateTimeOffset", typeof, parameterDefaultValue = false) ] + ProvidedStaticParameter("PreferDateTimeOffset", typeof, parameterDefaultValue = false) + ProvidedStaticParameter("ExceptionIfMissing", typeof, parameterDefaultValue = false) ] let helpText = """Typed representation of a XML file. @@ -255,7 +259,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this = 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. When true and a Schema is provided, the XSD complex type name is used for the generated F# type instead of the element name. This causes multiple elements that share the same XSD type to map to a single F# type. Defaults to false for backward compatibility. - When true, date-time strings without an explicit timezone offset are inferred as DateTimeOffset (using the local offset) instead of DateTime. Defaults to false.""" + When true, date-time strings without an explicit timezone offset are inferred as DateTimeOffset (using the local offset) instead of DateTime. Defaults to false. + When true, accessing a non-optional field that is missing in the XML data raises an exception instead of returning a default value (empty string for string, NaN for float). Defaults to false for backward compatibility.""" do xmlProvTy.AddXmlDoc helpText diff --git a/src/FSharp.Data.Json.Core/JsonRuntime.fs b/src/FSharp.Data.Json.Core/JsonRuntime.fs index 9d60c0ea8..2980d74ea 100644 --- a/src/FSharp.Data.Json.Core/JsonRuntime.fs +++ b/src/FSharp.Data.Json.Core/JsonRuntime.fs @@ -99,6 +99,27 @@ type JsonRuntime = failwithf "Expecting %s at '%s', got %s" (getTypeName ()) path <| x.ToString(JsonSaveOptions.DisableFormatting) + /// Operation that extracts the value from an option and always throws if the value is not present. + /// Used when ExceptionIfMissing=true to raise an exception for missing fields instead of returning defaults. + static member GetNonOptionalValueStrict<'T>(path: string, opt: option<'T>, originalValue) : 'T = + let getTypeName () = + let name = typeof<'T>.Name + + if name.StartsWith("i", StringComparison.OrdinalIgnoreCase) then + "an " + name + else + "a " + name + + match opt, originalValue with + | Some value, _ -> value + | None, Some((JsonValue.Array _ | JsonValue.Record _) as x) -> + failwithf "Expecting %s at '%s', got %s" (getTypeName ()) path + <| x.ToString(JsonSaveOptions.DisableFormatting) + | None, None -> failwithf "'%s' is missing" path + | None, Some x -> + failwithf "Expecting %s at '%s', got %s" (getTypeName ()) path + <| x.ToString(JsonSaveOptions.DisableFormatting) + /// Converts JSON array to array of target types static member ConvertArray<'T>(doc: IJsonDocument, mapping: Func) = match doc.JsonValue with diff --git a/src/FSharp.Data.Runtime.Utilities/TextRuntime.fs b/src/FSharp.Data.Runtime.Utilities/TextRuntime.fs index f248981fd..78b25b8b4 100644 --- a/src/FSharp.Data.Runtime.Utilities/TextRuntime.fs +++ b/src/FSharp.Data.Runtime.Utilities/TextRuntime.fs @@ -184,6 +184,14 @@ type TextRuntime = | None, None -> failwithf "%s is missing" name | None, Some originalValue -> failwithf "Expecting %s in %s, got %s" (typeof<'T>.Name) name originalValue + /// Operation that extracts the value from an option and always throws if the value is not present. + /// Used when ExceptionIfMissing=true to raise an exception for missing fields instead of returning defaults. + static member GetNonOptionalValueStrict<'T>(name: string, opt: option<'T>, originalValue) : 'T = + match opt, originalValue with + | Some value, _ -> value + | None, None -> failwithf "%s is missing" name + | None, Some originalValue -> failwithf "Expecting %s in %s, got %s" (typeof<'T>.Name) name originalValue + /// Turn an F# option type Option<'T> containing a primitive /// value type into a .NET type Nullable<'T> static member OptionToNullable opt = diff --git a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs index 995bb1ead..bcd21f309 100644 --- a/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs +++ b/tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs @@ -48,7 +48,8 @@ type internal XmlProviderArgs = UseOriginalNames : bool PreferOptionals : bool UseSchemaTypeNames : bool - PreferDateTimeOffset : bool } + PreferDateTimeOffset : bool + ExceptionIfMissing : bool } type internal JsonProviderArgs = { Sample : string @@ -66,7 +67,8 @@ type internal JsonProviderArgs = UseOriginalNames : bool OmitNullFields : bool PreferOptionals : bool - PreferDateTimeOffset : bool } + PreferDateTimeOffset : bool + ExceptionIfMissing : bool } type internal HtmlProviderArgs = { Sample : string @@ -133,7 +135,8 @@ type internal TypeProviderInstantiation = box x.UseOriginalNames box x.PreferOptionals box x.UseSchemaTypeNames - box x.PreferDateTimeOffset |] + box x.PreferDateTimeOffset + box x.ExceptionIfMissing |] | Json x -> (fun cfg -> new JsonProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -151,7 +154,8 @@ type internal TypeProviderInstantiation = box x.UseOriginalNames box x.OmitNullFields box x.PreferOptionals - box x.PreferDateTimeOffset |] + box x.PreferDateTimeOffset + box x.ExceptionIfMissing |] | Html x -> (fun cfg -> new HtmlProvider(cfg) :> TypeProviderForNamespaces), [| box x.Sample @@ -279,7 +283,8 @@ type internal TypeProviderInstantiation = UseOriginalNames = false PreferOptionals = true UseSchemaTypeNames = false - PreferDateTimeOffset = false } + PreferDateTimeOffset = false + ExceptionIfMissing = false } | "Json" -> // Handle special case for Schema.json tests where some fields might be empty if args.Length > 5 && not (String.IsNullOrEmpty(args.[5])) then @@ -298,7 +303,8 @@ type internal TypeProviderInstantiation = UseOriginalNames = false OmitNullFields = false PreferOptionals = true - PreferDateTimeOffset = false } + PreferDateTimeOffset = false + ExceptionIfMissing = false } else // This is for schema-based tests in the format "Json,,,,,true,false,BackwardCompatible,SimpleSchema.json" Json { Sample = args.[1] @@ -316,7 +322,8 @@ type internal TypeProviderInstantiation = UseOriginalNames = false OmitNullFields = false PreferOptionals = true - PreferDateTimeOffset = false } + PreferDateTimeOffset = false + ExceptionIfMissing = 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 21afc3d2e..05c94aaf5 100644 --- a/tests/FSharp.Data.Tests/JsonProvider.fs +++ b/tests/FSharp.Data.Tests/JsonProvider.fs @@ -1039,3 +1039,36 @@ let ``JsonProvider PreferOptionals=false uses empty string for missing string fi let doc = JsonPreferOptionalsFalse.Parse("""{"name": "Bob"}""") doc.Tag.GetType() |> should equal typeof doc.Tag |> should equal "" + +// ExceptionIfMissing tests + +type JsonExceptionIfMissing = JsonProvider<"""{"name": "Alice", "age": 30}""", ExceptionIfMissing=true> +type JsonExceptionIfMissingFalse = JsonProvider<"""{"name": "Alice", "age": 30}""", ExceptionIfMissing=false> +// Use a value that exceeds decimal range to force float inference +type JsonExceptionIfMissingFloat = JsonProvider<"""{"name": "Alice", "score": 9999999999999999999999999999999999.3}""", ExceptionIfMissing=true> +type JsonExceptionIfMissingFloatFalse = JsonProvider<"""{"name": "Alice", "score": 9999999999999999999999999999999999.3}""", ExceptionIfMissing=false> + +[] +let ``JsonProvider ExceptionIfMissing=true raises exception for missing string field`` () = + let doc = JsonExceptionIfMissing.Parse("""{"age": 30}""") + (fun () -> doc.Name |> ignore) |> should throw typeof + +[] +let ``JsonProvider ExceptionIfMissing=true raises exception for missing int field`` () = + let doc = JsonExceptionIfMissing.Parse("""{"name": "Alice"}""") + (fun () -> doc.Age |> ignore) |> should throw typeof + +[] +let ``JsonProvider ExceptionIfMissing=true raises exception for missing float field`` () = + let doc = JsonExceptionIfMissingFloat.Parse("""{"name": "Alice"}""") + (fun () -> doc.Score |> ignore) |> should throw typeof + +[] +let ``JsonProvider ExceptionIfMissing=false returns default for missing string field`` () = + let doc = JsonExceptionIfMissingFalse.Parse("""{"age": 30}""") + doc.Name |> should equal "" + +[] +let ``JsonProvider ExceptionIfMissing=false returns NaN for missing float field`` () = + let doc = JsonExceptionIfMissingFloatFalse.Parse("""{"name": "Alice"}""") + Double.IsNaN(doc.Score) |> should equal true diff --git a/tests/FSharp.Data.Tests/XmlProvider.fs b/tests/FSharp.Data.Tests/XmlProvider.fs index 323b4305e..b6837aa37 100644 --- a/tests/FSharp.Data.Tests/XmlProvider.fs +++ b/tests/FSharp.Data.Tests/XmlProvider.fs @@ -1417,3 +1417,19 @@ let ``XmlProvider UseSchemaTypeNames=true with shared-types.xsd: shipTo and bill order.Contact |> should equal None // Both elements share the same XSD AddressType, so the generated .NET types must match order.ShipTo.GetType() |> should equal (order.BillTo.GetType()) + + +// ExceptionIfMissing tests + +type XmlExceptionIfMissing = XmlProvider<"""""", ExceptionIfMissing=true> +type XmlExceptionIfMissingFalse = XmlProvider<"""""", ExceptionIfMissing=false> + +[] +let ``XmlProvider ExceptionIfMissing=true raises exception for missing attribute`` () = + let doc = XmlExceptionIfMissing.Parse("""""") + (fun () -> doc.Name |> ignore) |> should throw typeof + +[] +let ``XmlProvider ExceptionIfMissing=false returns empty string for missing attribute`` () = + let doc = XmlExceptionIfMissingFalse.Parse("""""") + doc.Name |> should equal "" From 32769654b50f99e2353cdcfeeea53d1c59deba86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 28 Feb 2026 12:31:02 +0000 Subject: [PATCH 2/2] ci: trigger CI checks