Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 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)
- 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)

## 8.0.0 - Feb 25 2026

Expand Down
12 changes: 9 additions & 3 deletions src/FSharp.Data.DesignTime/Xml/XmlProvider.fs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
let dtdProcessing = args.[11] :?> string
let useOriginalNames = args.[12] :?> bool
let preferOptionals = args.[13] :?> bool
let useSchemaTypeNames = args.[14] :?> bool

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

let t =
schemaSet |> XsdParsing.getElements |> List.ofSeq |> XsdInference.inferElements
schemaSet
|> XsdParsing.getElements
|> List.ofSeq
|> XsdInference.inferElements useSchemaTypeNames
#if NET6_0_OR_GREATER
if preferDateOnly && ProviderHelpers.runtimeSupportsNet6Types cfg.RuntimeAssembly then
t
Expand Down Expand Up @@ -205,7 +209,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
ProvidedStaticParameter("PreferDateOnly", typeof<bool>, parameterDefaultValue = false)
ProvidedStaticParameter("DtdProcessing", typeof<string>, parameterDefaultValue = "Ignore")
ProvidedStaticParameter("UseOriginalNames", typeof<bool>, parameterDefaultValue = false)
ProvidedStaticParameter("PreferOptionals", typeof<bool>, parameterDefaultValue = true) ]
ProvidedStaticParameter("PreferOptionals", typeof<bool>, parameterDefaultValue = true)
ProvidedStaticParameter("UseSchemaTypeNames", typeof<bool>, parameterDefaultValue = false) ]

let helpText =
"""<summary>Typed representation of a XML file.</summary>
Expand All @@ -232,7 +237,8 @@ type public XmlProvider(cfg: TypeProviderConfig) as this =
<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>
<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>
<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>
<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>"""
<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>
<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>"""


do xmlProvTy.AddXmlDoc helpText
Expand Down
41 changes: 27 additions & 14 deletions src/FSharp.Data.Xml.Core/XsdInference.fs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ module XsdModel =
| ComplexType of XsdComplexType

and [<ReferenceEquality>] XsdComplexType =
{ Attributes: (XmlQualifiedName * XmlTypeCode * IsOptional) list
{ Name: XmlQualifiedName option
Attributes: (XmlQualifiedName * XmlTypeCode * IsOptional) list
Contents: XsdContent }

and XsdContent =
Expand Down Expand Up @@ -150,7 +151,12 @@ module XsdParsing =
result

and parseComplexType ctx (x: XmlSchemaComplexType) =
{ Attributes =
{ Name =
if x.QualifiedName.IsEmpty then
None
else
Some x.QualifiedName
Attributes =
x.AttributeUses.Values
|> ofType<XmlSchemaAttribute>
|> Seq.filter (fun a -> a.Use <> XmlSchemaUse.Prohibited)
Expand Down Expand Up @@ -274,8 +280,14 @@ module internal XsdInference =
type InferenceContext = System.Collections.Generic.Dictionary<XsdComplexType, InferedProperty>

// derives an InferedType for an element definition
let rec inferElementType ctx elm =
let name = getElementName elm
let rec inferElementType useSchemaTypeNames ctx (elm: XsdElement) =
let name =
if useSchemaTypeNames then
match elm.Type with
| ComplexType cty when cty.Name.IsSome -> Some(formatName cty.Name.Value)
Comment thread
dsyme marked this conversation as resolved.
| _ -> getElementName elm
else
getElementName elm

if elm.IsAbstract then
InferedType.Record(name, [], optional = false)
Expand All @@ -287,7 +299,7 @@ module internal XsdInference =
let props = if elm.IsNillable then [ prop; nil ] else [ prop ]
InferedType.Record(name, props, optional = false)
| ComplexType cty ->
let props = inferProperties ctx cty
let props = inferProperties useSchemaTypeNames ctx cty

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


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

result.Type <-
match getElements ctx Single xsdParticle with
match getElements useSchemaTypeNames ctx Single xsdParticle with
| [] -> InferedType.Null
| items ->
let tags = items |> List.map (fst >> getRecordTag)

let types =
items
|> List.map (fun (e, m) -> m, inferElementType ctx e)
|> List.map (fun (e, m) -> m, inferElementType useSchemaTypeNames ctx e)
|> Seq.zip tags
|> Map.ofSeq

Expand All @@ -349,7 +361,7 @@ module internal XsdInference =
body :: attrs

// collects element definitions in a particle
and getElements ctx parentMultiplicity =
and getElements useSchemaTypeNames ctx parentMultiplicity =
function
| XsdParticle.Element(occ, elm) ->
let mult = combineMultiplicity (parentMultiplicity, getMultiplicity occ)
Expand All @@ -362,23 +374,24 @@ module internal XsdInference =
| XsdParticle.Sequence(occ, particles)
| XsdParticle.All(occ, particles) ->
let mult = combineMultiplicity (parentMultiplicity, getMultiplicity occ)
particles |> List.collect (getElements ctx mult)
particles |> List.collect (getElements useSchemaTypeNames ctx mult)
| XsdParticle.Choice(occ, particles) ->
let mult = makeOptional (getMultiplicity occ)
let mult' = combineMultiplicity (parentMultiplicity, mult)
particles |> List.collect (getElements ctx mult')
particles |> List.collect (getElements useSchemaTypeNames ctx mult')
| XsdParticle.Empty -> []
| XsdParticle.Any _ -> []


let inferElements elms =
let inferElements useSchemaTypeNames elms =
let ctx = InferenceContext()

match elms |> List.filter (fun elm -> not elm.IsAbstract) with
| [] -> failwith "No suitable element definition found in the schema."
| [ elm ] -> inferElementType ctx elm
| [ elm ] -> inferElementType useSchemaTypeNames ctx elm
| elms ->
elms
|> List.map (fun elm -> InferedTypeTag.Record(getElementName elm), inferElementType ctx elm)
|> List.map (fun elm ->
InferedTypeTag.Record(getElementName elm), inferElementType useSchemaTypeNames ctx elm)
|> Map.ofList
|> (fun x -> InferedType.Heterogeneous(x, false))
2 changes: 1 addition & 1 deletion tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ let internal getInferedTypeFromSchema xsd =
|> XmlSchema.parseSchema ""
|> XsdParsing.getElements
|> List.ofSeq
|> XsdInference.inferElements
|> XsdInference.inferElements false

let internal isValid xsd =
let xmlSchemaSet = XmlSchema.parseSchema "" xsd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ type internal XmlProviderArgs =
PreferDateOnly : bool
DtdProcessing : string
UseOriginalNames : bool
PreferOptionals : bool }
PreferOptionals : bool
UseSchemaTypeNames : bool }

type internal JsonProviderArgs =
{ Sample : string
Expand Down Expand Up @@ -126,7 +127,8 @@ type internal TypeProviderInstantiation =
box x.PreferDateOnly
box x.DtdProcessing
box x.UseOriginalNames
box x.PreferOptionals |]
box x.PreferOptionals
box x.UseSchemaTypeNames |]
| Json x ->
(fun cfg -> new JsonProvider(cfg) :> TypeProviderForNamespaces),
[| box x.Sample
Expand Down Expand Up @@ -268,7 +270,8 @@ type internal TypeProviderInstantiation =
PreferDateOnly = false
DtdProcessing = "Ignore"
UseOriginalNames = false
PreferOptionals = true }
PreferOptionals = true
UseSchemaTypeNames = 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
Expand Down
32 changes: 32 additions & 0 deletions tests/FSharp.Data.Tests/Data/shared-types.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- Schema demonstrating shared complex types referenced by multiple elements -->

<xs:element name="order" type="OrderType"/>

<xs:complexType name="OrderType">
<xs:sequence>
<xs:element name="shipTo" type="AddressType"/>
<xs:element name="billTo" type="AddressType"/>
<xs:element name="contact" type="PersonType" minOccurs="0"/>
</xs:sequence>
<xs:attribute name="id" type="xs:string" use="required"/>
</xs:complexType>

<xs:complexType name="AddressType">
<xs:sequence>
<xs:element name="street" type="xs:string"/>
<xs:element name="city" type="xs:string"/>
<xs:element name="zip" type="xs:string"/>
</xs:sequence>
<xs:attribute name="country" type="xs:string"/>
</xs:complexType>

<xs:complexType name="PersonType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="email" type="xs:string" minOccurs="0"/>
</xs:sequence>
</xs:complexType>

</xs:schema>
Loading