Skip to content

Commit bafa47f

Browse files
Add UseSchemaTypeNames parameter to XmlProvider for XSD type deduplication
When UseSchemaTypeNames=true and a Schema is provided, elements sharing the same XSD complexType are mapped to a single F# type (named after the XSD type) instead of generating a separate per-element type. For example, with po.xsd, shipTo and billTo (both of type USAddress) previously generated XmlProvider+ShipTo and XmlProvider+BillTo as separate identical F# types. With UseSchemaTypeNames=true, both map to a single XmlProvider+USAddress type. Closes #1488 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ff1df2a commit bafa47f

6 files changed

Lines changed: 76 additions & 21 deletions

File tree

RELEASE_NOTES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 8.1.0-beta
44

55
- 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)
6+
- Add `UseSchemaTypeNames` parameter to `XmlProvider`: when `true` and `Schema` is provided, multiple elements sharing the same XSD complex type generate a single F# type (named after the XSD type) instead of separate per-element types (closes #1488)
67

78
## 8.0.0 - Feb 25 2026
89

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
5454
let dtdProcessing = args.[11] :?> string
5555
let useOriginalNames = args.[12] :?> bool
5656
let preferOptionals = args.[13] :?> bool
57+
let useSchemaTypeNames = args.[14] :?> bool
5758

5859
let inferenceMode =
5960
InferenceMode'.FromPublicApi(inferenceMode, inferTypesFromValues)
@@ -79,7 +80,10 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
7980
use _holder = IO.logTime "Inference" sample
8081

8182
let t =
82-
schemaSet |> XsdParsing.getElements |> List.ofSeq |> XsdInference.inferElements
83+
schemaSet
84+
|> XsdParsing.getElements
85+
|> List.ofSeq
86+
|> XsdInference.inferElements useSchemaTypeNames
8387
#if NET6_0_OR_GREATER
8488
if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then
8589
t
@@ -205,7 +209,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
205209
ProvidedStaticParameter("PreferDateOnly", typeof<bool>, parameterDefaultValue = false)
206210
ProvidedStaticParameter("DtdProcessing", typeof<string>, parameterDefaultValue = "Ignore")
207211
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false)
208-
ProvidedStaticParameter("PreferOptionals", typeof<bool>, parameterDefaultValue = true) ]
212+
ProvidedStaticParameter("PreferOptionals", typeof<bool>, parameterDefaultValue = true)
213+
ProvidedStaticParameter("UseSchemaTypeNames", typeof<bool>, parameterDefaultValue = false) ]
209214

210215
let helpText =
211216
"""<summary>Typed representation of a XML file.</summary>
@@ -232,7 +237,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
232237
<param name='PreferDateOnly'>When true on .NET 6+, date-only strings are inferred as DateOnly and time-only strings as TimeOnly. Defaults to false for backward compatibility.</param>
233238
<param name='DtdProcessing'>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).</param>
234239
<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>
235-
<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>"""
240+
<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>
241+
<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>"""
236242

237243

238244
do xmlProvTy.AddXmlDoc helpText

src/FSharp.Data.Xml.Core/XsdInference.fs

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ module XsdModel =
4040
| ComplexType of XsdComplexType
4141

4242
and [<ReferenceEquality>] XsdComplexType =
43-
{ Attributes: (XmlQualifiedName * XmlTypeCode * IsOptional) list
43+
{ Name: XmlQualifiedName option
44+
Attributes: (XmlQualifiedName * XmlTypeCode * IsOptional) list
4445
Contents: XsdContent }
4546

4647
and XsdContent =
@@ -150,7 +151,12 @@ module XsdParsing =
150151
result
151152

152153
and parseComplexType ctx (x: XmlSchemaComplexType) =
153-
{ Attributes =
154+
{ Name =
155+
if x.QualifiedName.IsEmpty then
156+
None
157+
else
158+
Some x.QualifiedName
159+
Attributes =
154160
x.AttributeUses.Values
155161
|> ofType<XmlSchemaAttribute>
156162
|> Seq.filter (fun a -> a.Use <> XmlSchemaUse.Prohibited)
@@ -274,8 +280,14 @@ module internal XsdInference =
274280
type InferenceContext = System.Collections.Generic.Dictionary<XsdComplexType, InferedProperty>
275281

276282
// derives an InferedType for an element definition
277-
let rec inferElementType ctx elm =
278-
let name = getElementName elm
283+
let rec inferElementType useSchemaTypeNames ctx (elm: XsdElement) =
284+
let name =
285+
if useSchemaTypeNames then
286+
match elm.Type with
287+
| ComplexType cty when cty.Name.IsSome -> Some(formatName cty.Name.Value)
288+
| _ -> getElementName elm
289+
else
290+
getElementName elm
279291

280292
if elm.IsAbstract then
281293
InferedType.Record(name, [], optional = false)
@@ -287,7 +299,7 @@ module internal XsdInference =
287299
let props = if elm.IsNillable then [ prop; nil ] else [ prop ]
288300
InferedType.Record(name, props, optional = false)
289301
| ComplexType cty ->
290-
let props = inferProperties ctx cty
302+
let props = inferProperties useSchemaTypeNames ctx cty
291303

292304
let props =
293305
if elm.IsNillable then
@@ -301,7 +313,7 @@ module internal XsdInference =
301313
InferedType.Record(name, props, optional = false)
302314

303315

304-
and inferProperties (ctx: InferenceContext) cty =
316+
and inferProperties useSchemaTypeNames (ctx: InferenceContext) cty =
305317
let attrs: InferedProperty list =
306318
cty.Attributes
307319
|> List.map (fun (name, typeCode, optional) ->
@@ -328,14 +340,14 @@ module internal XsdInference =
328340
let getRecordTag (e: XsdElement) = InferedTypeTag.Record(getElementName e)
329341

330342
result.Type <-
331-
match getElements ctx Single xsdParticle with
343+
match getElements useSchemaTypeNames ctx Single xsdParticle with
332344
| [] -> InferedType.Null
333345
| items ->
334346
let tags = items |> List.map (fst >> getRecordTag)
335347

336348
let types =
337349
items
338-
|> List.map (fun (e, m) -> m, inferElementType ctx e)
350+
|> List.map (fun (e, m) -> m, inferElementType useSchemaTypeNames ctx e)
339351
|> Seq.zip tags
340352
|> Map.ofSeq
341353

@@ -349,7 +361,7 @@ module internal XsdInference =
349361
body :: attrs
350362

351363
// collects element definitions in a particle
352-
and getElements ctx parentMultiplicity =
364+
and getElements useSchemaTypeNames ctx parentMultiplicity =
353365
function
354366
| XsdParticle.Element(occ, elm) ->
355367
let mult = combineMultiplicity (parentMultiplicity, getMultiplicity occ)
@@ -362,23 +374,24 @@ module internal XsdInference =
362374
| XsdParticle.Sequence(occ, particles)
363375
| XsdParticle.All(occ, particles) ->
364376
let mult = combineMultiplicity (parentMultiplicity, getMultiplicity occ)
365-
particles |> List.collect (getElements ctx mult)
377+
particles |> List.collect (getElements useSchemaTypeNames ctx mult)
366378
| XsdParticle.Choice(occ, particles) ->
367379
let mult = makeOptional (getMultiplicity occ)
368380
let mult' = combineMultiplicity (parentMultiplicity, mult)
369-
particles |> List.collect (getElements ctx mult')
381+
particles |> List.collect (getElements useSchemaTypeNames ctx mult')
370382
| XsdParticle.Empty -> []
371383
| XsdParticle.Any _ -> []
372384

373385

374-
let inferElements elms =
386+
let inferElements useSchemaTypeNames elms =
375387
let ctx = InferenceContext()
376388

377389
match elms |> List.filter (fun elm -> not elm.IsAbstract) with
378390
| [] -> failwith "No suitable element definition found in the schema."
379-
| [ elm ] -> inferElementType ctx elm
391+
| [ elm ] -> inferElementType useSchemaTypeNames ctx elm
380392
| elms ->
381393
elms
382-
|> List.map (fun elm -> InferedTypeTag.Record(getElementName elm), inferElementType ctx elm)
394+
|> List.map (fun elm ->
395+
InferedTypeTag.Record(getElementName elm), inferElementType useSchemaTypeNames ctx elm)
383396
|> Map.ofList
384397
|> (fun x -> InferedType.Heterogeneous(x, false))

tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ let internal getInferedTypeFromSchema xsd =
444444
|> XmlSchema.parseSchema ""
445445
|> XsdParsing.getElements
446446
|> List.ofSeq
447-
|> XsdInference.inferElements
447+
|> XsdInference.inferElements false
448448

449449
let internal isValid xsd =
450450
let xmlSchemaSet = XmlSchema.parseSchema "" xsd

tests/FSharp.Data.DesignTime.Tests/TypeProviderInstantiation.fs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ type internal XmlProviderArgs =
4545
PreferDateOnly : bool
4646
DtdProcessing : string
4747
UseOriginalNames : bool
48-
PreferOptionals : bool }
48+
PreferOptionals : bool
49+
UseSchemaTypeNames : bool }
4950

5051
type internal JsonProviderArgs =
5152
{ Sample : string
@@ -126,7 +127,8 @@ type internal TypeProviderInstantiation =
126127
box x.PreferDateOnly
127128
box x.DtdProcessing
128129
box x.UseOriginalNames
129-
box x.PreferOptionals |]
130+
box x.PreferOptionals
131+
box x.UseSchemaTypeNames |]
130132
| Json x ->
131133
(fun cfg -> new JsonProvider(cfg) :> TypeProviderForNamespaces),
132134
[| box x.Sample
@@ -268,7 +270,8 @@ type internal TypeProviderInstantiation =
268270
PreferDateOnly = false
269271
DtdProcessing = "Ignore"
270272
UseOriginalNames = false
271-
PreferOptionals = true }
273+
PreferOptionals = true
274+
UseSchemaTypeNames = false }
272275
| "Json" ->
273276
// Handle special case for Schema.json tests where some fields might be empty
274277
if args.Length > 5 && not (String.IsNullOrEmpty(args.[5])) then
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
3+
<!-- Schema demonstrating shared complex types referenced by multiple elements -->
4+
5+
<xs:element name="order" type="OrderType"/>
6+
7+
<xs:complexType name="OrderType">
8+
<xs:sequence>
9+
<xs:element name="shipTo" type="AddressType"/>
10+
<xs:element name="billTo" type="AddressType"/>
11+
<xs:element name="contact" type="PersonType" minOccurs="0"/>
12+
</xs:sequence>
13+
<xs:attribute name="id" type="xs:string" use="required"/>
14+
</xs:complexType>
15+
16+
<xs:complexType name="AddressType">
17+
<xs:sequence>
18+
<xs:element name="street" type="xs:string"/>
19+
<xs:element name="city" type="xs:string"/>
20+
<xs:element name="zip" type="xs:string"/>
21+
</xs:sequence>
22+
<xs:attribute name="country" type="xs:string"/>
23+
</xs:complexType>
24+
25+
<xs:complexType name="PersonType">
26+
<xs:sequence>
27+
<xs:element name="name" type="xs:string"/>
28+
<xs:element name="email" type="xs:string" minOccurs="0"/>
29+
</xs:sequence>
30+
</xs:complexType>
31+
32+
</xs:schema>

0 commit comments

Comments
 (0)