-
Notifications
You must be signed in to change notification settings - Fork 1
/
JsonSchemaMapper.ReflectionHelpers.cs
358 lines (305 loc) · 15.7 KB
/
JsonSchemaMapper.ReflectionHelpers.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace JsonSchemaMapper;
#if EXPOSE_JSON_SCHEMA_MAPPER
public
#else
internal
#endif
static partial class JsonSchemaMapper
{
// Uses reflection to determine the element type of an enumerable or dictionary type
// Workaround for https://github.com/dotnet/runtime/issues/77306#issuecomment-2007887560
private static Type GetElementType(JsonTypeInfo typeInfo)
{
Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Enumerable or JsonTypeInfoKind.Dictionary);
return (Type)typeof(JsonTypeInfo).GetProperty("ElementType", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(typeInfo)!;
}
// The source generator currently doesn't populate attribute providers for properties
// cf. https://github.com/dotnet/runtime/issues/100095
// Work around the issue by running a query for the relevant MemberInfo using the internal MemberName property
// https://github.com/dotnet/runtime/blob/de774ff9ee1a2c06663ab35be34b755cd8d29731/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs#L206
[SuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
Justification = "Members that already part of the source generated contract will not have been trimmed away.")]
private static ICustomAttributeProvider? ResolveAttributeProvider(JsonTypeInfo typeInfo, JsonPropertyInfo propertyInfo)
{
if (propertyInfo.AttributeProvider is { } provider)
{
return provider;
}
PropertyInfo memberNameProperty = typeof(JsonPropertyInfo).GetProperty("MemberName", BindingFlags.Instance | BindingFlags.NonPublic)!;
var memberName = (string?)memberNameProperty.GetValue(propertyInfo);
if (memberName is not null)
{
return typeInfo.Type.GetMember(memberName, MemberTypes.Property | MemberTypes.Field, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic).FirstOrDefault();
}
return null;
}
// Uses reflection to determine any custom converters specified for the element of a nullable type.
private static JsonConverter? ExtractCustomNullableConverter(JsonConverter? converter)
{
Debug.Assert(converter is null || IsBuiltInConverter(converter));
// There is unfortunately no way in which we can obtain the element converter from a nullable converter without resorting to private reflection
// https://github.com/dotnet/runtime/blob/5fda47434cecc590095e9aef3c4e560b7b7ebb47/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/NullableConverter.cs#L15-L17
if (converter != null && converter.GetType().Name == "NullableConverter`1")
{
FieldInfo? elementConverterField = converter.GetType().GetField("_elementConverter", BindingFlags.Instance | BindingFlags.NonPublic);
Debug.Assert(elementConverterField != null);
return (JsonConverter)elementConverterField!.GetValue(converter)!;
}
return null;
}
// Uses reflection to determine serialization configuration for enum types
// cf. https://github.com/dotnet/runtime/blob/5fda47434cecc590095e9aef3c4e560b7b7ebb47/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs#L23-L25
private static bool TryGetStringEnumConverterValues(JsonTypeInfo typeInfo, JsonConverter converter, out JsonArray? values)
{
Debug.Assert(typeInfo.Type.IsEnum && IsBuiltInConverter(converter));
if (converter is JsonConverterFactory factory)
{
converter = factory.CreateConverter(typeInfo.Type, typeInfo.Options)!;
}
FieldInfo? converterOptionsField = converter.GetType().GetField("_converterOptions", BindingFlags.Instance | BindingFlags.NonPublic);
FieldInfo? namingPolicyField = converter.GetType().GetField("_namingPolicy", BindingFlags.Instance | BindingFlags.NonPublic);
Debug.Assert(converterOptionsField != null);
Debug.Assert(namingPolicyField != null);
const int EnumConverterOptionsAllowStrings = 1;
var converterOptions = (int)converterOptionsField!.GetValue(converter)!;
if ((converterOptions & EnumConverterOptionsAllowStrings) != 0)
{
if (typeInfo.Type.GetCustomAttribute<FlagsAttribute>() is not null)
{
// For enums implemented as flags do not surface values in the JSON schema.
values = null;
}
else
{
var namingPolicy = (JsonNamingPolicy?)namingPolicyField!.GetValue(converter)!;
string[] names = Enum.GetNames(typeInfo.Type);
values = new JsonArray();
foreach (string name in names)
{
string effectiveName = namingPolicy?.ConvertName(name) ?? name;
values.Add((JsonNode)effectiveName);
}
}
return true;
}
values = null;
return false;
}
// Resolves the parameters of the deserialization constructor for a type, if they exist.
[SuppressMessage("Trimming", "IL2072:Target parameter argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
Justification = "The deserialization constructor should have already been referenced by the source generator and therefore will not have been trimmed.")]
private static Func<JsonPropertyInfo, ParameterInfo?> ResolveJsonConstructorParameterMapper(JsonTypeInfo typeInfo)
{
Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.Object);
if (typeInfo.Properties.Count > 0 &&
typeInfo.CreateObject is null && // Ensure that a default constructor isn't being used
typeInfo.Type.TryGetDeserializationConstructor(useDefaultCtorInAnnotatedStructs: true, out ConstructorInfo? ctor))
{
ParameterInfo[]? parameters = ctor?.GetParameters();
if (parameters?.Length > 0)
{
Dictionary<ParameterLookupKey, ParameterInfo> dict = new(parameters.Length);
foreach (ParameterInfo parameter in parameters)
{
if (parameter.Name is not null)
{
// We don't care about null parameter names or conflicts since they
// would have already been rejected by JsonTypeInfo configuration.
dict[new(parameter.Name, parameter.ParameterType)] = parameter;
}
}
return prop => dict.TryGetValue(new(prop.Name, prop.PropertyType), out ParameterInfo? parameter) ? parameter : null;
}
}
return static _ => null;
}
// Parameter to property matching semantics as declared in
// https://github.com/dotnet/runtime/blob/12d96ccfaed98e23c345188ee08f8cfe211c03e7/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs#L1007-L1030
private readonly struct ParameterLookupKey : IEquatable<ParameterLookupKey>
{
public ParameterLookupKey(string name, Type type)
{
Name = name;
Type = type;
}
public string Name { get; }
public Type Type { get; }
public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Name);
public bool Equals(ParameterLookupKey other) => Type == other.Type && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase);
public override bool Equals(object? obj) => obj is ParameterLookupKey key && Equals(key);
}
// Resolves the deserialization constructor for a type using logic copied from
// https://github.com/dotnet/runtime/blob/e12e2fa6cbdd1f4b0c8ad1b1e2d960a480c21703/src/libraries/System.Text.Json/Common/ReflectionExtensions.cs#L227-L286
private static bool TryGetDeserializationConstructor(
#if NETCOREAPP
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)]
#endif
this Type type,
bool useDefaultCtorInAnnotatedStructs,
out ConstructorInfo? deserializationCtor)
{
ConstructorInfo? ctorWithAttribute = null;
ConstructorInfo? publicParameterlessCtor = null;
ConstructorInfo? lonePublicCtor = null;
ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
if (constructors.Length == 1)
{
lonePublicCtor = constructors[0];
}
foreach (ConstructorInfo constructor in constructors)
{
if (HasJsonConstructorAttribute(constructor))
{
if (ctorWithAttribute != null)
{
deserializationCtor = null;
return false;
}
ctorWithAttribute = constructor;
}
else if (constructor.GetParameters().Length == 0)
{
publicParameterlessCtor = constructor;
}
}
// Search for non-public ctors with [JsonConstructor].
foreach (ConstructorInfo constructor in type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance))
{
if (HasJsonConstructorAttribute(constructor))
{
if (ctorWithAttribute != null)
{
deserializationCtor = null;
return false;
}
ctorWithAttribute = constructor;
}
}
// Structs will use default constructor if attribute isn't used.
if (useDefaultCtorInAnnotatedStructs && type.IsValueType && ctorWithAttribute == null)
{
deserializationCtor = null;
return true;
}
deserializationCtor = ctorWithAttribute ?? publicParameterlessCtor ?? lonePublicCtor;
return true;
static bool HasJsonConstructorAttribute(ConstructorInfo constructorInfo) =>
constructorInfo.GetCustomAttribute<JsonConstructorAttribute>() != null;
}
private static bool IsBuiltInConverter(JsonConverter converter) =>
converter.GetType().Assembly == typeof(JsonConverter).Assembly;
private static bool TryGetNullableElement(Type type, [NotNullWhen(true)] out Type? elementType)
{
if (type.IsValueType && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
elementType = type.GetGenericArguments()[0];
return true;
}
elementType = null;
return false;
}
// Resolves the nullable reference type annotations for a property or field,
// additionally addressing a few known bugs of the NullabilityInfo pre .NET 9.
private static NullabilityInfo GetMemberNullability(this NullabilityInfoContext context, MemberInfo memberInfo)
{
Debug.Assert(memberInfo is PropertyInfo or FieldInfo);
return memberInfo is PropertyInfo prop
? context.Create(prop)
: context.Create((FieldInfo)memberInfo);
}
private static NullabilityState GetParameterNullability(this NullabilityInfoContext context, ParameterInfo parameterInfo)
{
// Workaround for https://github.com/dotnet/runtime/issues/92487
if (parameterInfo.GetGenericParameterDefinition() is { ParameterType: { IsGenericParameter: true } typeParam })
{
// Step 1. Look for nullable annotations on the type parameter.
if (GetNullableFlags(typeParam) is byte[] flags)
{
return TranslateByte(flags[0]);
}
// Step 2. Look for nullable annotations on the generic method declaration.
if (typeParam.DeclaringMethod != null && GetNullableContextFlag(typeParam.DeclaringMethod) is byte flag)
{
return TranslateByte(flag);
}
// Step 3. Look for nullable annotations on the generic method declaration.
if (GetNullableContextFlag(typeParam.DeclaringType!) is byte flag2)
{
return TranslateByte(flag2);
}
// Default to nullable.
return NullabilityState.Nullable;
static byte[]? GetNullableFlags(MemberInfo member)
{
Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr =>
{
Type attrType = attr.GetType();
return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableAttribute";
});
return (byte[])attr?.GetType().GetField("NullableFlags")?.GetValue(attr)!;
}
static byte? GetNullableContextFlag(MemberInfo member)
{
Attribute? attr = member.GetCustomAttributes().FirstOrDefault(attr =>
{
Type attrType = attr.GetType();
return attrType.Namespace == "System.Runtime.CompilerServices" && attrType.Name == "NullableContextAttribute";
});
return (byte?)attr?.GetType().GetField("Flag")?.GetValue(attr)!;
}
static NullabilityState TranslateByte(byte b) =>
b switch
{
1 => NullabilityState.NotNull,
2 => NullabilityState.Nullable,
_ => NullabilityState.Unknown
};
}
return context.Create(parameterInfo).WriteState;
}
private static ParameterInfo GetGenericParameterDefinition(this ParameterInfo parameter)
{
if (parameter.Member is { DeclaringType.IsConstructedGenericType: true }
or MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false })
{
var genericMethod = (MethodBase)parameter.Member.GetGenericMemberDefinition()!;
return genericMethod.GetParameters()[parameter.Position];
}
return parameter;
}
[SuppressMessage("Trimming", "IL2075:'this' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method. The return value of the source method does not have matching annotations.",
Justification = "Looking up the generic member definition of the provided member.")]
private static MemberInfo GetGenericMemberDefinition(this MemberInfo member)
{
if (member is Type type)
{
return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type;
}
if (member.DeclaringType!.IsConstructedGenericType)
{
const BindingFlags AllMemberFlags =
BindingFlags.Static | BindingFlags.Instance |
BindingFlags.Public | BindingFlags.NonPublic;
return member.DeclaringType.GetGenericTypeDefinition()
.GetMember(member.Name, AllMemberFlags)
.First(m => m.MetadataToken == member.MetadataToken);
}
if (member is MethodInfo { IsGenericMethod: true, IsGenericMethodDefinition: false } method)
{
return method.GetGenericMethodDefinition();
}
return member;
}
}