Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
18 changes: 4 additions & 14 deletions src/FSharp.Data.Json.Core/JsonInference.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ open FSharp.Data.Runtime.StructuralInference
/// here we just need to infer types of primitive JSON values.
let rec internal inferType unitsOfMeasureProvider inferenceMode cultureInfo parentName json =
let inline inRangeDecimal lo hi (v: decimal) : bool = (v >= decimal lo) && (v <= decimal hi)
let inline inRangeFloat lo hi (v: float) : bool = (v >= float lo) && (v <= float hi)
let inline isIntegerDecimal (v: decimal) : bool = Math.Round v = v
let inline isIntegerFloat (v: float) : bool = Math.Round v = v

let shouldInferNonStringFromValue =
match inferenceMode with
Expand Down Expand Up @@ -49,18 +47,10 @@ let rec internal inferType unitsOfMeasureProvider inferenceMode cultureInfo pare
->
InferedType.Primitive(typeof<int64>, None, false, false)
| JsonValue.Number _ -> InferedType.Primitive(typeof<decimal>, None, false, false)
| JsonValue.Float f when
shouldInferNonStringFromValue
&& inRangeFloat Int32.MinValue Int32.MaxValue f
&& isIntegerFloat f
->
InferedType.Primitive(typeof<int>, None, false, false)
| JsonValue.Float f when
shouldInferNonStringFromValue
&& inRangeFloat Int64.MinValue Int64.MaxValue f
&& isIntegerFloat f
->
InferedType.Primitive(typeof<int64>, None, false, false)
// JsonValue.Float is produced when the JSON number uses exponential notation (e.g. 0.1e1, 2.34E5)
// because TextConversions.AsDecimal uses NumberStyles.Currency which excludes AllowExponent.
// Such values are always inferred as float regardless of whether the value happens to be a whole
// number, so that e.g. [0.1e1, 0.2e1] is inferred as float[] not int[]. See issue #1221.
| JsonValue.Float _ -> InferedType.Primitive(typeof<float>, None, false, false)
// More interesting types
| JsonValue.Array ar ->
Expand Down
12 changes: 12 additions & 0 deletions tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ let ``Finds common subtype of numeric types (float)``() =
let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source
actual |> should equal expected

// Regression test for https://github.com/fsprojects/FSharp.Data/issues/1221
// Exponential-notation JSON numbers (parsed as JsonValue.Float) should always infer as float,
// not be promoted to int/int64 when the value happens to be a whole number.
[<Test>]
let ``Exponential-notation numbers infer as float not int (issue 1221)``() =
// 0.1e1 = 1.0, 0.2e1 = 2.0 β€” these are stored as JsonValue.Float because
// TextConversions.AsDecimal (NumberStyles.Currency) does not allow exponent notation
let source = JsonValue.Parse """[ 0.1e1, 0.2e1, 1e3 ]"""
let expected = SimpleCollection(InferedType.Primitive(typeof<float>, None, false, false))
let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source
actual |> should equal expected

[<Test>]
let ``Infers heterogeneous type of InferedType.Primitives and records``() =
let source = JsonValue.Parse """[ {"a":0}, 1,2 ]"""
Expand Down
13 changes: 7 additions & 6 deletions tests/FSharp.Data.Tests/JsonProvider.fs
Original file line number Diff line number Diff line change
Expand Up @@ -816,17 +816,18 @@ let ``ParseList return result list`` () =
let prov = NumericFields.ParseList(""" [{"a":123}, {"a":987}] """)
prov |> Array.map (fun v -> v.A) |> Array.sort |> should equal [|123M; 987M|]

// Regression test for https://github.com/fsprojects/FSharp.Data/issues/1230
// Regression test for https://github.com/fsprojects/FSharp.Data/issues/1230 / #1221
// When a JSON array sample mixes decimal and exponential-notation numbers, the inferred
// type is decimal (because the exponential value is stored as JsonValue.Float and inferred
// as integer, which is then unified with decimal). At runtime, any exponential-notation
// number in the actual JSON must also be convertible to decimal.
// type is float. Exponential-notation numbers (e.g. 2.34567E5) are stored as JsonValue.Float
// (because TextConversions.AsDecimal uses NumberStyles.Currency which excludes AllowExponent)
// and are always inferred as float, not promoted to int/int64 as in earlier versions.
// This means [1, 2.34567E5, 3.14] is inferred as float[].
type ExponentialDecimalProvider = JsonProvider<"""{"mydata": [1, 2.34567E5, 3.14]}""">

[<Test>]
let ``Decimal inferred from mixed-notation array can parse exponential notation at runtime`` () =
let ``Mixed-notation array with exponential numbers is inferred as float`` () =
let result = ExponentialDecimalProvider.Parse("""{"mydata": [2, 3.45678E5, 9.01]}""")
result.Mydata |> should equal [| 2M; 345678M; 9.01M |]
result.Mydata |> should equal [| 2.0; 345678.0; 9.01 |]


type ServiceResponse = JsonProvider<"""[
Expand Down
Loading