Skip to content

Commit f671f35

Browse files
github-actions[bot]Repo AssistCopilotdsyme
authored
[Repo Assist] Add ExceptionIfMissing static parameter to JsonProvider and XmlProvider (#1680)
* 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> * ci: trigger CI checks --------- Co-authored-by: Repo Assist <repo-assist@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com>
1 parent 0a3b96b commit f671f35

14 files changed

Lines changed: 159 additions & 35 deletions

File tree

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 8.1.0-beta
44

5+
- 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.
56
- Add `Http.ParseLinkHeader` utility for parsing RFC 5988 `Link` response headers (used by GitHub, GitLab, and other paginated APIs) into a `Map<string, string>` from relation name to URL (closes #805)
67
- 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)
78
- Make `Http.AppendQueryToUrl` public (closes #1325)

src/FSharp.Data.DesignTime/CommonProviderImplementation/ConversionsGenerator.fs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ let getBackConversionQuotation missingValuesStr cultureStr typ value : Expr<stri
7575

7676
/// Creates a function that takes Expr<string option> and converts it to
7777
/// an expression of other type - the type is specified by `field`
78-
let internal convertStringValue missingValuesStr cultureStr (field: PrimitiveInferedProperty) =
78+
let internal convertStringValue missingValuesStr cultureStr exceptionIfMissing (field: PrimitiveInferedProperty) =
7979
let fieldName = field.Name
8080
let field = field.Value
8181

@@ -95,16 +95,20 @@ let internal convertStringValue missingValuesStr cultureStr (field: PrimitiveInf
9595
let convert value =
9696
getConversionQuotation missingValuesStr cultureStr field.InferedType value
9797

98+
let getNonOptionalValueMethod =
99+
if exceptionIfMissing then
100+
nameof (TextRuntime.GetNonOptionalValueStrict)
101+
else
102+
nameof (TextRuntime.GetNonOptionalValue)
103+
98104
match field.TypeWrapper with
99105
| TypeWrapper.None ->
100106
//prevent value being calculated twice
101107
let var = Var("value", typeof<string option>)
102108
let varExpr = Expr.Cast<string option>(Expr.Var var)
103109

104110
let body =
105-
typeof<TextRuntime>?(nameof (TextRuntime.GetNonOptionalValue))
106-
field.RuntimeType
107-
(fieldName, convert varExpr, varExpr)
111+
typeof<TextRuntime>?(getNonOptionalValueMethod) field.RuntimeType (fieldName, convert varExpr, varExpr)
108112

109113
Expr.Let(var, value, body)
110114
| TypeWrapper.Option -> convert value

src/FSharp.Data.DesignTime/Csv/CsvGenerator.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ module internal CsvTypeBuilder =
3333
inferredFields
3434
|> List.mapi (fun index field ->
3535
let typ, typWithoutMeasure, conv, convBack =
36-
ConversionsGenerator.convertStringValue missingValuesStr cultureStr field
36+
ConversionsGenerator.convertStringValue missingValuesStr cultureStr false field
3737

3838
let propertyName =
3939
if useOriginalNames then

src/FSharp.Data.DesignTime/Html/HtmlGenerator.fs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ module internal HtmlGenerator =
6767
columns
6868
|> List.mapi (fun index field ->
6969
let typ, typWithoutMeasure, conv, _convBack =
70-
ConversionsGenerator.convertStringValue missingValuesStr cultureStr field
70+
ConversionsGenerator.convertStringValue missingValuesStr cultureStr false field
7171

7272
{ TypeForTuple = typWithoutMeasure
7373
ProvidedProperty =
@@ -165,6 +165,7 @@ module internal HtmlGenerator =
165165
ConversionsGenerator.convertStringValue
166166
missingValuesStr
167167
cultureStr
168+
false
168169
(StructuralTypes.PrimitiveInferedProperty.Create("", typ, optional, None))
169170

170171
typ, conv
@@ -173,6 +174,7 @@ module internal HtmlGenerator =
173174
ConversionsGenerator.convertStringValue
174175
missingValuesStr
175176
cultureStr
177+
false
176178
(StructuralTypes.PrimitiveInferedProperty.Create("", typeof<string>, false, None))
177179

178180
typ, conv
@@ -238,6 +240,7 @@ module internal HtmlGenerator =
238240
ConversionsGenerator.convertStringValue
239241
missingValuesStr
240242
cultureStr
243+
false
241244
(StructuralTypes.PrimitiveInferedProperty.Create("", typ, optional, None))
242245

243246
typ, conv
@@ -246,6 +249,7 @@ module internal HtmlGenerator =
246249
ConversionsGenerator.convertStringValue
247250
missingValuesStr
248251
cultureStr
252+
false
249253
(StructuralTypes.PrimitiveInferedProperty.Create("", typeof<String>, false, None))
250254

251255
typ, conv

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ let internal convertJsonValue
5656
missingValuesStr
5757
cultureStr
5858
canPassAllConversionCallingTypes
59+
exceptionIfMissing
5960
(field: PrimitiveInferedValue)
6061
=
6162

@@ -80,15 +81,21 @@ let internal convertJsonValue
8081
let convert value =
8182
getConversionQuotation missingValuesStr cultureStr field.InferedType value
8283

84+
let getNonOptionalValueMethod =
85+
if exceptionIfMissing then
86+
nameof (JsonRuntime.GetNonOptionalValueStrict)
87+
else
88+
nameof (JsonRuntime.GetNonOptionalValue)
89+
8390
match field.TypeWrapper, canPassAllConversionCallingTypes with
8491
| TypeWrapper.None, true ->
8592
wrapInLetIfNeeded value (fun (varExpr: Expr<JsonValueOptionAndPath>) ->
86-
typeof<JsonRuntime>?(nameof (JsonRuntime.GetNonOptionalValue))
93+
typeof<JsonRuntime>?(getNonOptionalValueMethod)
8794
(field.RuntimeType)
8895
(<@ (%varExpr).Path @>, convert <@ (%varExpr).JsonOpt @>, <@ (%varExpr).JsonOpt @>))
8996
| TypeWrapper.None, false ->
9097
wrapInLetIfNeeded value (fun (varExpr: Expr<IJsonDocument>) ->
91-
typeof<JsonRuntime>?(nameof (JsonRuntime.GetNonOptionalValue))
98+
typeof<JsonRuntime>?(getNonOptionalValueMethod)
9299
(field.RuntimeType)
93100
(<@ (%varExpr).Path() @>, convert <@ Some (%varExpr).JsonValue @>, <@ Some (%varExpr).JsonValue @>))
94101
| TypeWrapper.Option, true -> convert <@ (%%value: JsonValue option) @>

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ type internal JsonGenerationContext =
3232
InferenceMode: InferenceMode'
3333
UnitsOfMeasureProvider: IUnitsOfMeasureProvider
3434
UseOriginalNames: bool
35-
OmitNullFields: bool }
35+
OmitNullFields: bool
36+
ExceptionIfMissing: bool }
3637

3738
static member Create
3839
(
@@ -44,7 +45,8 @@ type internal JsonGenerationContext =
4445
?typeCache,
4546
?preferDictionaries,
4647
?useOriginalNames,
47-
?omitNullFields
48+
?omitNullFields,
49+
?exceptionIfMissing
4850
) =
4951
let useOriginalNames = defaultArg useOriginalNames false
5052

@@ -56,6 +58,7 @@ type internal JsonGenerationContext =
5658
let typeCache = defaultArg typeCache (Dictionary())
5759
let preferDictionaries = defaultArg preferDictionaries false
5860
let omitNullFields = defaultArg omitNullFields false
61+
let exceptionIfMissing = defaultArg exceptionIfMissing false
5962

6063
JsonGenerationContext.Create(
6164
cultureStr,
@@ -67,7 +70,8 @@ type internal JsonGenerationContext =
6770
inferenceMode,
6871
unitsOfMeasureProvider,
6972
useOriginalNames,
70-
omitNullFields
73+
omitNullFields,
74+
exceptionIfMissing
7175
)
7276

7377
static member Create
@@ -81,7 +85,8 @@ type internal JsonGenerationContext =
8185
inferenceMode,
8286
unitsOfMeasureProvider,
8387
useOriginalNames,
84-
omitNullFields
88+
omitNullFields,
89+
exceptionIfMissing
8590
) =
8691
{ CultureStr = cultureStr
8792
TypeProviderType = tpType
@@ -95,7 +100,8 @@ type internal JsonGenerationContext =
95100
InferenceMode = inferenceMode
96101
UnitsOfMeasureProvider = unitsOfMeasureProvider
97102
UseOriginalNames = useOriginalNames
98-
OmitNullFields = omitNullFields }
103+
OmitNullFields = omitNullFields
104+
ExceptionIfMissing = exceptionIfMissing }
99105

100106
static member Create
101107
(
@@ -119,6 +125,7 @@ type internal JsonGenerationContext =
119125
inferenceMode,
120126
unitsOfMeasureProvider,
121127
useOriginalNames,
128+
false,
122129
false
123130
)
124131

@@ -367,7 +374,7 @@ module JsonTypeBuilder =
367374

368375
let typ, conv, conversionCallingType =
369376
PrimitiveInferedValue.Create(inferedType, optional, unit)
370-
|> convertJsonValue "" ctx.CultureStr canPassAllConversionCallingTypes
377+
|> convertJsonValue "" ctx.CultureStr canPassAllConversionCallingTypes ctx.ExceptionIfMissing
371378

372379
{ ConvertedType = typ
373380
OptionalConverter = Some conv

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
6262
let omitNullFields = args.[13] :?> bool
6363
let preferOptionals = args.[14] :?> bool
6464
let preferDateTimeOffset = args.[15] :?> bool
65+
let exceptionIfMissing = args.[16] :?> bool
6566

6667
let inferenceMode =
6768
InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues)
@@ -136,7 +137,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
136137
inferenceMode,
137138
?preferDictionaries = Some preferDictionaries,
138139
?useOriginalNames = Some useOriginalNames,
139-
?omitNullFields = Some omitNullFields
140+
?omitNullFields = Some omitNullFields,
141+
?exceptionIfMissing = Some exceptionIfMissing
140142
)
141143

142144
let result = JsonTypeBuilder.generateJsonType ctx false false rootName inferedType
@@ -187,7 +189,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
187189
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false)
188190
ProvidedStaticParameter("OmitNullFields", typeof<bool>, parameterDefaultValue = false)
189191
ProvidedStaticParameter("PreferOptionals", typeof<bool>, parameterDefaultValue = true)
190-
ProvidedStaticParameter("PreferDateTimeOffset", typeof<bool>, parameterDefaultValue = false) ]
192+
ProvidedStaticParameter("PreferDateTimeOffset", typeof<bool>, parameterDefaultValue = false)
193+
ProvidedStaticParameter("ExceptionIfMissing", typeof<bool>, parameterDefaultValue = false) ]
191194

192195
let helpText =
193196
"""<summary>Typed representation of a JSON document.</summary>
@@ -215,7 +218,8 @@ type public JsonProvider(cfg: TypeProviderConfig) as this =
215218
<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>
216219
<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>
217220
<param name='PreferOptionals'>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.</param>
218-
<param name='PreferDateTimeOffset'>When true, date-time strings without an explicit timezone offset are inferred as DateTimeOffset (using the local offset) instead of DateTime. Defaults to false.</param>"""
221+
<param name='PreferDateTimeOffset'>When true, date-time strings without an explicit timezone offset are inferred as DateTimeOffset (using the local offset) instead of DateTime. Defaults to false.</param>
222+
<param name='ExceptionIfMissing'>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.</param>"""
219223

220224
do jsonProvTy.AddXmlDoc helpText
221225
do jsonProvTy.DefineStaticParameters(parameters, buildTypes)

src/FSharp.Data.DesignTime/Xml/XmlGenerator.fs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ type internal XmlGenerationContext =
3030
UnifyGlobally: bool
3131
XmlTypeCache: Dictionary<InferedType, XmlGenerationResult>
3232
JsonTypeCache: Dictionary<InferedType, ProvidedTypeDefinition>
33-
UseOriginalNames: bool }
33+
UseOriginalNames: bool
34+
ExceptionIfMissing: bool }
3435

35-
static member Create(unitsOfMeasureProvider, inferenceMode, cultureStr, tpType, unifyGlobally, useOriginalNames) =
36+
static member Create
37+
(unitsOfMeasureProvider, inferenceMode, cultureStr, tpType, unifyGlobally, useOriginalNames, ?exceptionIfMissing) =
3638
let niceName = if useOriginalNames then id else NameUtils.nicePascalName
3739
let uniqueNiceName = NameUtils.uniqueGenerator niceName
3840
uniqueNiceName "XElement" |> ignore
41+
let exceptionIfMissing = defaultArg exceptionIfMissing false
3942

4043
{ CultureStr = cultureStr
4144
UnitsOfMeasureProvider = unitsOfMeasureProvider
@@ -45,15 +48,18 @@ type internal XmlGenerationContext =
4548
UnifyGlobally = unifyGlobally
4649
XmlTypeCache = Dictionary()
4750
JsonTypeCache = Dictionary()
48-
UseOriginalNames = useOriginalNames }
51+
UseOriginalNames = useOriginalNames
52+
ExceptionIfMissing = exceptionIfMissing }
4953

5054
member x.ConvertValue prop =
51-
let typ, _, conv, _ = ConversionsGenerator.convertStringValue "" x.CultureStr prop
55+
let typ, _, conv, _ =
56+
ConversionsGenerator.convertStringValue "" x.CultureStr x.ExceptionIfMissing prop
57+
5258
typ, conv
5359

5460
member x.ConvertValueBack prop =
5561
let typ, _, _, convBack =
56-
ConversionsGenerator.convertStringValue "" x.CultureStr prop
62+
ConversionsGenerator.convertStringValue "" x.CultureStr x.ExceptionIfMissing prop
5763

5864
typ, convBack
5965

@@ -155,7 +161,8 @@ module internal XmlTypeBuilder =
155161
ctx.UnitsOfMeasureProvider,
156162
ctx.InferenceMode,
157163
ctx.UniqueNiceName,
158-
ctx.JsonTypeCache
164+
ctx.JsonTypeCache,
165+
?exceptionIfMissing = Some ctx.ExceptionIfMissing
159166
)
160167

161168
let result = JsonTypeBuilder.generateJsonType ctx false true "" typ

src/FSharp.Data.DesignTime/Xml/XmlProvider.fs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
5555
let useOriginalNames = args.[12] :?> bool
5656
let preferOptionals = args.[13] :?> bool
5757
let useSchemaTypeNames = args.[14] :?> bool
58-
let preferDateTimeOffset = args.[14] :?> bool
58+
let preferDateTimeOffset = args.[15] :?> bool
59+
let exceptionIfMissing = args.[16] :?> bool
5960

6061
let inferenceMode =
6162
InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues)
@@ -110,7 +111,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
110111
cultureStr,
111112
tpType,
112113
globalInference || schema <> "",
113-
useOriginalNames
114+
useOriginalNames,
115+
exceptionIfMissing
114116
)
115117

116118
let result = XmlTypeBuilder.generateXmlType ctx inferedType
@@ -174,7 +176,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
174176
cultureStr,
175177
tpType,
176178
globalInference || schema <> "",
177-
useOriginalNames
179+
useOriginalNames,
180+
exceptionIfMissing
178181
)
179182

180183
let result = XmlTypeBuilder.generateXmlType ctx inferedType
@@ -226,7 +229,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
226229
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false)
227230
ProvidedStaticParameter("PreferOptionals", typeof<bool>, parameterDefaultValue = true)
228231
ProvidedStaticParameter("UseSchemaTypeNames", typeof<bool>, parameterDefaultValue = false)
229-
ProvidedStaticParameter("PreferDateTimeOffset", typeof<bool>, parameterDefaultValue = false) ]
232+
ProvidedStaticParameter("PreferDateTimeOffset", typeof<bool>, parameterDefaultValue = false)
233+
ProvidedStaticParameter("ExceptionIfMissing", typeof<bool>, parameterDefaultValue = false) ]
230234

231235
let helpText =
232236
"""<summary>Typed representation of a XML file.</summary>
@@ -255,7 +259,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
255259
<param name='UseOriginalNames'>When true, XML element and attribute names are used as-is for generated property names instead of being normalized to PascalCase. Defaults to false.</param>
256260
<param name='PreferOptionals'>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.</param>
257261
<param name='UseSchemaTypeNames'>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.</param>
258-
<param name='PreferDateTimeOffset'>When true, date-time strings without an explicit timezone offset are inferred as DateTimeOffset (using the local offset) instead of DateTime. Defaults to false.</param>"""
262+
<param name='PreferDateTimeOffset'>When true, date-time strings without an explicit timezone offset are inferred as DateTimeOffset (using the local offset) instead of DateTime. Defaults to false.</param>
263+
<param name='ExceptionIfMissing'>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.</param>"""
259264

260265

261266
do xmlProvTy.AddXmlDoc helpText

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,27 @@ type JsonRuntime =
9999
failwithf "Expecting %s at '%s', got %s" (getTypeName ()) path
100100
<| x.ToString(JsonSaveOptions.DisableFormatting)
101101

102+
/// Operation that extracts the value from an option and always throws if the value is not present.
103+
/// Used when ExceptionIfMissing=true to raise an exception for missing fields instead of returning defaults.
104+
static member GetNonOptionalValueStrict<'T>(path: string, opt: option<'T>, originalValue) : 'T =
105+
let getTypeName () =
106+
let name = typeof<'T>.Name
107+
108+
if name.StartsWith("i", StringComparison.OrdinalIgnoreCase) then
109+
"an " + name
110+
else
111+
"a " + name
112+
113+
match opt, originalValue with
114+
| Some value, _ -> value
115+
| None, Some((JsonValue.Array _ | JsonValue.Record _) as x) ->
116+
failwithf "Expecting %s at '%s', got %s" (getTypeName ()) path
117+
<| x.ToString(JsonSaveOptions.DisableFormatting)
118+
| None, None -> failwithf "'%s' is missing" path
119+
| None, Some x ->
120+
failwithf "Expecting %s at '%s', got %s" (getTypeName ()) path
121+
<| x.ToString(JsonSaveOptions.DisableFormatting)
122+
102123
/// Converts JSON array to array of target types
103124
static member ConvertArray<'T>(doc: IJsonDocument, mapping: Func<IJsonDocument, 'T>) =
104125
match doc.JsonValue with

0 commit comments

Comments
 (0)