-
Notifications
You must be signed in to change notification settings - Fork 19
/
NodaConverterBase.cs
135 lines (121 loc) · 6.36 KB
/
NodaConverterBase.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
// Copyright 2012 The Noda Time Authors. All rights reserved.
// Use of this source code is governed by the Apache License 2.0,
// as found in the LICENSE.txt file.
using System;
using System.Reflection;
using Newtonsoft.Json;
using NodaTime.Utility;
namespace NodaTime.Serialization.JsonNet
{
/// <summary>
/// Base class for all the Json.NET converters which handle value types (which is most of them).
/// This handles all the boilerplate code dealing with nullity.
/// </summary>
/// <typeparam name="T">The type to convert to/from JSON.</typeparam>
public abstract class NodaConverterBase<T> : JsonConverter
{
/// <summary>
/// Default constructor.
/// </summary>
protected NodaConverterBase()
{
}
// For value types and sealed classes, we can optimize and not call IsAssignableFrom.
private static readonly bool CheckAssignableFrom =
!(typeof(T).IsValueType || (typeof(T).IsClass && typeof(T).IsSealed));
private static readonly Type NullableT = typeof(T).IsValueType
? typeof(Nullable<>).MakeGenericType(typeof(T)) : typeof(T);
// TODO: It's not clear whether we *should* support inheritance here. The Json.NET docs
// aren't clear on when this is used - is it for reading or writing? If it's for both, that's
// a problem: our "writer" may be okay for subclasses, but that doesn't mean the "reader" is.
// This may well only be an issue for DateTimeZone, as everything else uses a sealed type (e.g. Period)
// or a value type.
/// <summary>
/// Returns whether or not this converter supports the given type.
/// </summary>
/// <param name="objectType">The type to check for compatibility.</param>
/// <returns>True if the given type is supported by this converter (including the nullable form for
/// value types); false otherwise.</returns>
public override bool CanConvert(Type objectType) =>
objectType == typeof(T) || objectType == NullableT ||
(CheckAssignableFrom && typeof(T).IsAssignableFrom(objectType.GetTypeInfo()));
/// <summary>
/// Converts the JSON stored in a reader into the relevant Noda Time type.
/// </summary>
/// <param name="reader">The Json.NET reader to read data from.</param>
/// <param name="objectType">The type to convert the JSON to.</param>
/// <param name="existingValue">An existing value; ignored by this converter.</param>
/// <param name="serializer">A serializer to use for any embedded deserialization.</param>
/// <exception cref="InvalidNodaDataException">The JSON was invalid for this converter.</exception>
/// <returns>The deserialized value.</returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
Preconditions.CheckData(objectType == NullableT,
"Cannot convert null value to {0}",
objectType);
return null;
}
// Handle empty strings automatically
if (reader.TokenType == JsonToken.String)
{
string value = (string) reader.Value;
if (value == "")
{
Preconditions.CheckData(objectType == NullableT,
"Cannot convert null value to {0}",
objectType);
return null;
}
}
try
{
// Delegate to the concrete subclass. At this point we know that we don't want to return null, so we
// can ask the subclass to return a T, which we will box. That will be valid even if objectType is
// T? because the boxed form of a non-null T? value is just the boxed value itself.
// Note that we don't currently pass existingValue down; we could change this if we ever found a use for it.
return ReadJsonImpl(reader, serializer);
}
catch (Exception ex)
{
throw new JsonSerializationException($"Cannot convert value to {objectType}", ex);
}
}
/// <summary>
/// Implemented by concrete subclasses, this performs the final conversion from a non-null JSON value to
/// a value of type T.
/// </summary>
/// <param name="reader">The JSON reader to pull data from</param>
/// <param name="serializer">The serializer to use for nested serialization</param>
/// <returns>The deserialized value of type T.</returns>
protected abstract T ReadJsonImpl(JsonReader reader, JsonSerializer serializer);
/// <summary>
/// Writes the given value to a Json.NET writer.
/// </summary>
/// <param name="writer">The writer to write the JSON to.</param>
/// <param name="value">The value to write.</param>
/// <param name="serializer">The serializer to use for any embedded serialization.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Json.NET should prevent this happening, but let's validate...
Preconditions.CheckNotNull(value, nameof(value));
// Note: don't need to worry about value is T? due to the way boxing works.
// Again, Json.NET probably prevents us from needing to check this, really.
if (value is T castValue)
{
WriteJsonImpl(writer, castValue, serializer);
return;
}
throw new ArgumentException($"Unexpected value when converting. Expected {typeof(T).FullName}, got {value.GetType().FullName}.");
}
/// <summary>
/// Implemented by concrete subclasses, this performs the final write operation for a non-null value of type T
/// to JSON.
/// </summary>
/// <param name="writer">The writer to write JSON data to</param>
/// <param name="value">The value to serialize</param>
/// <param name="serializer">The serializer to use for nested serialization</param>
protected abstract void WriteJsonImpl(JsonWriter writer, T value, JsonSerializer serializer);
}
}