Skip to content

Commit 4e45606

Browse files
authored
Merge pull request #101 from Tarmil/property-order
Fix #98: Implement JsonPropertyOrderAttribute on records
2 parents 1160377 + 3c50697 commit 4e45606

2 files changed

Lines changed: 63 additions & 4 deletions

File tree

src/FSharp.SystemTextJson/Record.fs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type internal RecordProperty =
1616
MustBeNonNull: bool
1717
MustBePresent: bool
1818
IsSkip: obj -> bool
19+
WriteOrder: int
1920
}
2021

2122
type internal IRecordConverter =
@@ -28,9 +29,34 @@ type JsonRecordConverter<'T>(options: JsonSerializerOptions, fsOptions: JsonFSha
2829

2930
let recordType: Type = typeof<'T>
3031

32+
let fields = FSharpType.GetRecordFields(recordType, true)
33+
34+
let fieldOrderIndices =
35+
let revIndices =
36+
fields
37+
|> Array.mapi (fun i field ->
38+
let revI =
39+
match field.GetCustomAttributes(typeof<JsonPropertyOrderAttribute>, true) with
40+
| [| :? JsonPropertyOrderAttribute as attr |] -> attr.Order
41+
| _ -> 0
42+
struct (revI, i))
43+
if revIndices |> Array.exists (fun struct (revI, _) -> revI <> 0) then
44+
// Using Seq.sort rather than Array.sort because it is stable
45+
let revIndices =
46+
revIndices
47+
|> Seq.sortBy (fun struct (revI, _) -> revI)
48+
|> Array.ofSeq
49+
let res = Array.zeroCreate fields.Length
50+
for i in 0..res.Length-1 do
51+
let struct (_, x) = revIndices[i]
52+
res[x] <- i
53+
ValueSome res
54+
else
55+
ValueNone
56+
3157
let fieldProps =
32-
FSharpType.GetRecordFields(recordType, true)
33-
|> Array.map (fun p ->
58+
fields
59+
|> Array.mapi (fun i p ->
3460
let name =
3561
match p.GetCustomAttributes(typeof<JsonPropertyNameAttribute>, true) with
3662
| [| :? JsonPropertyNameAttribute as name |] -> name.Name
@@ -57,9 +83,15 @@ type JsonRecordConverter<'T>(options: JsonSerializerOptions, fsOptions: JsonFSha
5783
MustBeNonNull = not canBeNull
5884
MustBePresent = not canBeSkipped
5985
IsSkip = isSkip p.PropertyType
86+
WriteOrder = match fieldOrderIndices with ValueSome a -> a[i] | ValueNone -> i
6087
}
6188
)
6289

90+
let writeOrderedFieldProps =
91+
let a = Array.mapi (fun i x -> struct (i, x)) fieldProps
92+
a |> Array.sortInPlaceBy (fun struct (_, x) -> x.WriteOrder)
93+
a
94+
6395
let fieldCount = fieldProps.Length
6496
let minExpectedFieldCount =
6597
fieldProps
@@ -150,9 +182,8 @@ type JsonRecordConverter<'T>(options: JsonSerializerOptions, fsOptions: JsonFSha
150182

151183
member internal _.WriteRestOfObject(writer, value, options) =
152184
let values = dector value
153-
for i in 0..fieldProps.Length-1 do
185+
for struct (i, p) in writeOrderedFieldProps do
154186
let v = values[i]
155-
let p = fieldProps[i]
156187
if not p.Ignore && not (options.IgnoreNullValues && isNull v) && not (p.IsSkip v) then
157188
writer.WritePropertyName(p.Name)
158189
JsonSerializer.Serialize(writer, v, p.Type, options)

tests/FSharp.SystemTextJson.Tests/Test.Record.fs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,34 @@ module NonStruct =
247247
Assert.Throws<JsonException>(fun () -> JsonSerializer.Deserialize<Override>("""{"x":null}""", o) |> ignore)
248248
|> ignore
249249

250+
type OrderedClass() =
251+
[<JsonPropertyOrder 2>]
252+
member val LastName = "" with get, set
253+
member val Country = "" with get, set
254+
member val City = "" with get, set
255+
[<JsonPropertyOrder -1>]
256+
member val Id = 0 with get, set
257+
[<JsonPropertyOrder 1>]
258+
member val FirstName = "" with get, set
259+
260+
type Ordered =
261+
{
262+
[<JsonPropertyOrder 2>]
263+
LastName: string
264+
Country: string
265+
City: string
266+
[<JsonPropertyOrder -1>]
267+
Id: int
268+
[<JsonPropertyOrder 1>]
269+
FirstName: string
270+
}
271+
272+
[<Fact>]
273+
let ``should respect JsonPropertyOrder`` () =
274+
let expected = OrderedClass(LastName="Dupont",City="Paris",Country="France",Id=123,FirstName="Jean")
275+
let actual = {LastName="Dupont";City="Paris";Country="France";Id=123;FirstName="Jean"}
276+
Assert.Equal(JsonSerializer.Serialize(expected), JsonSerializer.Serialize(actual, options))
277+
250278
module Struct =
251279

252280
[<Struct; JsonFSharpConverter>]

0 commit comments

Comments
 (0)