Skip to content

Commit 594502b

Browse files
committed
Implement record deserialization with Reflection.Emit
1 parent ba953ca commit 594502b

5 files changed

Lines changed: 121 additions & 84 deletions

File tree

src/FSharp.SystemTextJson/FSharp.SystemTextJson.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
</PropertyGroup>
66
<ItemGroup>
77
<Compile Include="TypeCache.fs" />
8-
<Compile Include="RecordField.fs" />
8+
<Compile Include="Record.Reflection.fs" />
99
<Compile Include="Record.fs" />
1010
<Compile Include="Union.fs" />
1111
<Compile Include="All.fs" />
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
namespace System.Text.Json.Serialization
2+
3+
open System
4+
open System.Reflection
5+
open FSharp.Reflection
6+
open System.Reflection.Emit
7+
open System.Text.Json
8+
9+
type internal Serializer = delegate of Utf8JsonWriter * obj * JsonSerializerOptions -> unit
10+
type internal Deserializer = delegate of byref<Utf8JsonReader> * obj * JsonSerializerOptions -> unit
11+
12+
type internal RecordField<'Record> =
13+
{
14+
Name: string
15+
Type: Type
16+
Ignore: bool
17+
Serialize: Serializer
18+
Deserialize: Deserializer
19+
}
20+
21+
module internal RecordReflection =
22+
23+
let private name (p: PropertyInfo) =
24+
match p.GetCustomAttributes(typeof<JsonPropertyNameAttribute>, true) with
25+
| [| :? JsonPropertyNameAttribute as name |] -> name.Name
26+
| _ -> p.Name
27+
28+
let private isIgnore (p: PropertyInfo) =
29+
p.GetCustomAttributes(typeof<JsonIgnoreAttribute>, true)
30+
|> Array.isEmpty
31+
|> not
32+
33+
let private deserializer<'Field> (f: FieldInfo) =
34+
let setter =
35+
let dynMethod =
36+
new DynamicMethod(
37+
f.Name,
38+
typeof<Void>,
39+
[| typeof<obj>; f.FieldType |],
40+
typedefof<RecordField<_>>.Module,
41+
skipVisibility = true
42+
)
43+
let gen = dynMethod.GetILGenerator()
44+
gen.Emit(OpCodes.Ldarg_0)
45+
if f.DeclaringType.IsValueType then
46+
gen.Emit(OpCodes.Unbox, f.DeclaringType)
47+
gen.Emit(OpCodes.Ldarg_1)
48+
gen.Emit(OpCodes.Stfld, f)
49+
gen.Emit(OpCodes.Ret)
50+
dynMethod.CreateDelegate(typeof<Action<obj, 'Field>>) :?> Action<obj, 'Field>
51+
Deserializer(fun reader record options ->
52+
let value = JsonSerializer.Deserialize<'Field>(&reader, options)
53+
setter.Invoke(record, value))
54+
55+
let private serializer<'Field> (f: FieldInfo) =
56+
let getter =
57+
let dynMethod =
58+
new DynamicMethod(
59+
f.Name,
60+
f.FieldType,
61+
[| typeof<obj> |],
62+
typedefof<RecordField<_>>.Module,
63+
skipVisibility = true
64+
)
65+
let gen = dynMethod.GetILGenerator()
66+
gen.Emit(OpCodes.Ldarg_0)
67+
if f.DeclaringType.IsValueType then
68+
gen.Emit(OpCodes.Unbox, f.DeclaringType)
69+
gen.Emit(OpCodes.Ldfld, f)
70+
gen.Emit(OpCodes.Ret)
71+
dynMethod.CreateDelegate(typeof<Func<obj, 'Field>>) :?> Func<obj, 'Field>
72+
Serializer(fun writer record options ->
73+
let v = getter.Invoke(record)
74+
JsonSerializer.Serialize<'Field>(writer, v, options)
75+
)
76+
77+
let private thisModule = typedefof<RecordField<_>>.Assembly.GetType("System.Text.Json.Serialization.RecordReflection")
78+
79+
let fields<'Record> () =
80+
let recordTy = typeof<'Record>
81+
let fields = recordTy.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic)
82+
let props = FSharpType.GetRecordFields(recordTy, true)
83+
(fields, props)
84+
||> Array.map2 (fun f p ->
85+
let serializer =
86+
thisModule.GetMethod("serializer", BindingFlags.Static ||| BindingFlags.NonPublic)
87+
.MakeGenericMethod(p.PropertyType)
88+
.Invoke(null, [|f|])
89+
:?> Serializer
90+
let deserializer =
91+
thisModule.GetMethod("deserializer", BindingFlags.Static ||| BindingFlags.NonPublic)
92+
.MakeGenericMethod(p.PropertyType)
93+
.Invoke(null, [|f|])
94+
:?> Deserializer
95+
{
96+
Name = name p
97+
Type = p.PropertyType
98+
Ignore = isIgnore p
99+
Serialize = serializer
100+
Deserialize = deserializer
101+
} : RecordField<'Record>
102+
)

src/FSharp.SystemTextJson/Record.fs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,25 @@ open FSharp.Reflection
88
type JsonRecordConverter<'T>() =
99
inherit JsonConverter<'T>()
1010

11-
static let fieldProps = RecordField<'T>.fields()
11+
static let ty = typeof<'T>
12+
13+
static let fields = RecordReflection.fields<'T>()
1214

1315
static let expectedFieldCount =
14-
fieldProps
16+
fields
1517
|> Seq.filter (fun p -> not p.Ignore)
1618
|> Seq.length
1719

18-
static let ctor = FSharpValue.PreComputeRecordConstructor(typeof<'T>, true)
20+
static let ctor() =
21+
FormatterServices.GetUninitializedObject(ty)
1922

2023
static let fieldIndex (reader: byref<Utf8JsonReader>) =
2124
let mutable found = ValueNone
2225
let mutable i = 0
23-
while found.IsNone && i < fieldProps.Length do
24-
let p = fieldProps.[i]
26+
while found.IsNone && i < fields.Length do
27+
let p = fields.[i]
2528
if reader.ValueTextEquals(p.Name.AsSpan()) then
26-
found <- ValueSome (struct (i, p))
29+
found <- ValueSome p
2730
else
2831
i <- i + 1
2932
found
@@ -32,7 +35,7 @@ type JsonRecordConverter<'T>() =
3235
if reader.TokenType <> JsonTokenType.StartObject then
3336
raise (JsonException("Failed to parse record type " + typeToConvert.FullName + ", expected JSON object, found " + string reader.TokenType))
3437

35-
let fields = Array.zeroCreate fieldProps.Length
38+
let res = ctor()
3639
let mutable cont = true
3740
let mutable fieldsFound = 0
3841
while cont && reader.Read() do
@@ -41,20 +44,20 @@ type JsonRecordConverter<'T>() =
4144
cont <- false
4245
| JsonTokenType.PropertyName ->
4346
match fieldIndex &reader with
44-
| ValueSome (i, p) when not p.Ignore ->
47+
| ValueSome p when not p.Ignore ->
4548
fieldsFound <- fieldsFound + 1
46-
fields.[i] <- JsonSerializer.Deserialize(&reader, p.Type, options)
49+
p.Deserialize.Invoke(&reader, res, options)
4750
| _ ->
4851
reader.Skip()
4952
| _ -> ()
5053

5154
if fieldsFound < expectedFieldCount then
5255
raise (JsonException("Missing field for record type " + typeToConvert.FullName))
53-
ctor fields :?> 'T
56+
res :?> 'T
5457

5558
override __.Write(writer, value, options) =
5659
writer.WriteStartObject()
57-
for p in fieldProps do
60+
for p in fields do
5861
if not p.Ignore then
5962
writer.WritePropertyName(p.Name)
6063
p.Serialize.Invoke(writer, value, options)

src/FSharp.SystemTextJson/RecordField.fs

Lines changed: 0 additions & 68 deletions
This file was deleted.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ module NonStruct =
2525

2626
type B =
2727
{
28-
bx: int
28+
bx: uint32
2929
by: string
3030
}
3131

@@ -35,11 +35,11 @@ module NonStruct =
3535
[<Fact>]
3636
let ``deserialize via options`` () =
3737
let actual = JsonSerializer.Deserialize("""{"bx":1,"by":"b"}""", options)
38-
Assert.Equal({bx=1;by="b"}, actual)
38+
Assert.Equal({bx=1u;by="b"}, actual)
3939

4040
[<Fact>]
4141
let ``serialize via options`` () =
42-
let actual = JsonSerializer.Serialize({bx=1;by="b"}, options)
42+
let actual = JsonSerializer.Serialize({bx=1u;by="b"}, options)
4343
Assert.Equal("""{"bx":1,"by":"b"}""", actual)
4444

4545
type C =
@@ -50,7 +50,7 @@ module NonStruct =
5050
[<Fact>]
5151
let ``deserialize nested`` () =
5252
let actual = JsonSerializer.Deserialize("""{"cx":{"bx":1,"by":"b"}}""", options)
53-
Assert.Equal({cx={bx=1;by="b"}}, actual)
53+
Assert.Equal({cx={bx=1u;by="b"}}, actual)
5454

5555
[<Fact>]
5656
let ``deserialize anonymous`` () =

0 commit comments

Comments
 (0)