Skip to content

Commit ee95202

Browse files
Repo AssistCopilot
authored andcommitted
YamlProvider: quoted YAML scalars always infer as string even when InferTypesFromValues=true
YAML's quoting style is semantically meaningful: zip: "01234" explicitly marks the value as a string (to preserve the leading zero), even though the bare value looks numeric. Add yamlNodeToJsonValueForInference / parseYamlForInference which substitutes a plain-letter sentinel value for any quoted scalar that would otherwise be re-inferred as a numeric/date/bool type by JsonInference. The runtime parseYaml path is unchanged, so the actual value is preserved. Addresses @dsyme's request on PR #1646. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 6d14e20 commit ee95202

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

src/FSharp.Data.Yaml.Core/YamlDocument.fs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,110 @@ open YamlDotNet.RepresentationModel
1818

1919
module internal YamlConversions =
2020

21+
// For design-time type inference: quoted YAML scalars should always be typed as
22+
// strings, even when InferTypesFromValues=true. We use a non-numeric sentinel value
23+
// so that JsonInference does not re-infer "01234" as int, etc.
24+
let rec yamlNodeToJsonValueForInference (node: YamlNode) : JsonValue =
25+
match node with
26+
| :? YamlMappingNode as mapping ->
27+
let props =
28+
[| for kvp in mapping.Children do
29+
let key =
30+
match kvp.Key with
31+
| :? YamlScalarNode as s -> s.Value
32+
| other -> other.ToString()
33+
34+
yield (key, yamlNodeToJsonValueForInference kvp.Value) |]
35+
36+
JsonValue.Record props
37+
38+
| :? YamlSequenceNode as sequence ->
39+
let elements =
40+
[| for item in sequence.Children -> yamlNodeToJsonValueForInference item |]
41+
42+
JsonValue.Array elements
43+
44+
| :? YamlScalarNode as scalar ->
45+
let value = scalar.Value
46+
47+
if value = null then
48+
JsonValue.Null
49+
else
50+
match scalar.Style with
51+
| YamlDotNet.Core.ScalarStyle.SingleQuoted
52+
| YamlDotNet.Core.ScalarStyle.DoubleQuoted
53+
| YamlDotNet.Core.ScalarStyle.Literal
54+
| YamlDotNet.Core.ScalarStyle.Folded ->
55+
// Explicitly quoted scalars are always strings in YAML.
56+
// Use the original value if it is clearly non-numeric; otherwise substitute
57+
// a plain-letter sentinel so that value-based type inference sees a string.
58+
let sentinel =
59+
match
60+
System.Decimal.TryParse(
61+
value,
62+
System.Globalization.NumberStyles.Float,
63+
System.Globalization.CultureInfo.InvariantCulture
64+
)
65+
with
66+
| true, _ -> "s"
67+
| false, _ ->
68+
match
69+
System.Double.TryParse(
70+
value,
71+
System.Globalization.NumberStyles.Float,
72+
System.Globalization.CultureInfo.InvariantCulture
73+
)
74+
with
75+
| true, _ -> "s"
76+
| false, _ -> value
77+
78+
JsonValue.String sentinel
79+
| _ ->
80+
// Plain scalars: use the same type-aware conversion as runtime
81+
if value = "null" || value = "~" || value = "" then
82+
JsonValue.Null
83+
elif value = "true" || value = "True" || value = "TRUE" then
84+
JsonValue.Boolean true
85+
elif value = "false" || value = "False" || value = "FALSE" then
86+
JsonValue.Boolean false
87+
elif value = ".inf" || value = ".Inf" || value = ".INF" || value = "+.inf" then
88+
JsonValue.Float System.Double.PositiveInfinity
89+
elif value = "-.inf" || value = "-.Inf" || value = "-.INF" then
90+
JsonValue.Float System.Double.NegativeInfinity
91+
elif value = ".nan" || value = ".NaN" || value = ".NAN" then
92+
JsonValue.Float System.Double.NaN
93+
else
94+
match
95+
System.Decimal.TryParse(
96+
value,
97+
System.Globalization.NumberStyles.Float,
98+
System.Globalization.CultureInfo.InvariantCulture
99+
)
100+
with
101+
| true, d -> JsonValue.Number d
102+
| false, _ ->
103+
match
104+
System.Double.TryParse(
105+
value,
106+
System.Globalization.NumberStyles.Float,
107+
System.Globalization.CultureInfo.InvariantCulture
108+
)
109+
with
110+
| true, f -> JsonValue.Float f
111+
| false, _ -> JsonValue.String value
112+
113+
| _ -> JsonValue.Null
114+
115+
let parseYamlForInference (text: string) : JsonValue =
116+
let yaml = YamlStream()
117+
use reader = new StringReader(text)
118+
yaml.Load(reader)
119+
120+
if yaml.Documents.Count = 0 then
121+
JsonValue.Null
122+
else
123+
yamlNodeToJsonValueForInference yaml.Documents.[0].RootNode
124+
21125
let rec yamlNodeToJsonValue (node: YamlNode) : JsonValue =
22126
match node with
23127
| :? YamlMappingNode as mapping ->
@@ -189,6 +293,20 @@ type YamlDocument =
189293
IsError = false)>]
190294
static member ParseToJsonValue(text: string) : JsonValue = YamlConversions.parseYaml text
191295

296+
/// <summary>
297+
/// Like ParseToJsonValue but for design-time type inference: explicitly quoted YAML scalars
298+
/// are represented as string sentinels so that InferTypesFromValues does not re-infer
299+
/// "01234" as int, etc.
300+
/// </summary>
301+
/// <exclude />
302+
[<EditorBrowsableAttribute(EditorBrowsableState.Never)>]
303+
[<CompilerMessageAttribute("This method is intended for use in generated code only.",
304+
10001,
305+
IsHidden = true,
306+
IsError = false)>]
307+
static member ParseToJsonValueForInference(text: string) : JsonValue =
308+
YamlConversions.parseYamlForInference text
309+
192310
/// <exclude />
193311
[<EditorBrowsableAttribute(EditorBrowsableState.Never)>]
194312
[<CompilerMessageAttribute("This method is intended for use in generated code only.",

0 commit comments

Comments
 (0)