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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,66 @@ namespace Stripe.Infrastructure
using System.Text.Json;
using System.Text.Json.Serialization;

/// <summary>
/// A JsonConverterFactory that handles both DateTime and DateTime? types.
/// This factory creates specialized converters for each type to properly handle
/// null values for nullable DateTime properties (fixes issue #3157).
/// </summary>
#pragma warning disable SA1649 // File name should match first type name
internal class STJUnixDateTimeConverter : JsonConverterFactory
#pragma warning restore SA1649 // File name should match first type name
{
/// <inheritdoc/>
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
}

/// <inheritdoc/>
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (typeToConvert == typeof(DateTime?))
{
return new STJUnixNullableDateTimeConverter();
}

return new STJUnixDateTimeConverterImpl();
}
}

/// <summary>
/// Converter for nullable DateTime? that properly handles null JSON values.
/// </summary>
#pragma warning disable SA1402 // File may only contain a single type
internal class STJUnixNullableDateTimeConverter : JsonConverter<DateTime?>
#pragma warning restore SA1402 // File may only contain a single type
{
private static readonly STJUnixDateTimeConverterImpl BaseConverter = new STJUnixDateTimeConverterImpl();

/// <inheritdoc/>
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}

return BaseConverter.Read(ref reader, typeToConvert, options);
}

/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
{
if (value == null)
{
writer.WriteNullValue();
return;
}

BaseConverter.Write(writer, value.Value, options);
}
}

/// <summary>
/// Converts a <see cref="DateTime"/> to and from Unix epoch time.
/// </summary>
Expand All @@ -13,7 +73,9 @@ namespace Stripe.Infrastructure
/// Newtonsoft.Json 11.0. Once we bump the minimum version of Newtonsoft.Json to 11.0, we can
/// start using the provided converter and get rid of this class.
/// </remarks>
internal class STJUnixDateTimeConverter : JsonConverter<DateTime>
#pragma warning disable SA1402 // File may only contain a single type
internal class STJUnixDateTimeConverterImpl : JsonConverter<DateTime>
#pragma warning restore SA1402 // File may only contain a single type
{
/// <summary>
/// Reads the JSON representation of the object.
Expand All @@ -24,7 +86,6 @@ internal class STJUnixDateTimeConverter : JsonConverter<DateTime>
/// <returns>The object value.</returns>
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
bool nullable = IsNullable(typeToConvert);
long seconds;

if (reader.TokenType == JsonTokenType.Number)
Expand Down Expand Up @@ -73,16 +134,7 @@ public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, Jso
/// <param name="options">The calling serializer's options.</param>
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
{
long seconds;

if (value is DateTime dateTime)
{
seconds = (long)(dateTime.ToUniversalTime() - DateTimeUtils.UnixEpoch).TotalSeconds;
}
else
{
throw new JsonException("Expected date object value.");
}
long seconds = (long)(value.ToUniversalTime() - DateTimeUtils.UnixEpoch).TotalSeconds;

if (seconds < 0)
{
Expand All @@ -91,21 +143,6 @@ public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializer

writer.WriteNumberValue(seconds);
}

private static bool IsNullable(Type t)
{
if (t == null)
{
throw new ArgumentNullException(nameof(t));
}

if (t.IsValueType)
{
return t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>);
}

return true;
}
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ internal class SerializablePropertyInfo
private static MethodInfo createGetDelegateMethod = typeof(SerializablePropertyInfo).GetMethod("CreateGetDelegate", BindingFlags.Static | BindingFlags.NonPublic);
private static MethodInfo createSetDelegateMethod = typeof(SerializablePropertyInfo).GetMethod("CreateSetDelegate", BindingFlags.Static | BindingFlags.NonPublic);
private static MethodInfo getConverterForTypeMethod = typeof(SerializablePropertyInfo).GetMethod("GetConverterForType", BindingFlags.Static | BindingFlags.NonPublic);
private static MethodInfo getConverterFromFactoryMethod = typeof(SerializablePropertyInfo).GetMethod("GetConverterFromFactory", BindingFlags.Static | BindingFlags.NonPublic);
private static MethodInfo getDefaultConverterMethod = typeof(SerializablePropertyInfo).GetMethod("GetDefaultConverter", BindingFlags.Static | BindingFlags.NonPublic);

private Func<object, object> getDelegate = null;
Expand Down Expand Up @@ -102,20 +103,28 @@ internal JsonConverter<object> GetConverter(JsonSerializerOptions options)
{
if (this.getConverter == null)
{
var customConverter = default(JsonConverter<object>);
Func<JsonSerializerOptions, JsonConverter<object>> customConverter = null;
if (this.CustomConverterType != null)
{
// this assumes any property-level JsonConverter attribute
// specifies a JsonConverter<> type and not a JsonConverterFactory
// type
var baseType = this.CustomConverterType.BaseType;
var cvtGenericMethod = getConverterForTypeMethod.MakeGenericMethod(baseType, baseType.GenericTypeArguments[0]);
customConverter = (JsonConverter<object>)cvtGenericMethod.Invoke(null, new object[] { this.CustomConverterType });
if (baseType == typeof(JsonConverterFactory))
{
// Handle JsonConverterFactory types (fixes issue #3157)
var cvtGenericMethod = getConverterFromFactoryMethod.MakeGenericMethod(this.PropertyInfo.PropertyType);
customConverter = (Func<JsonSerializerOptions, JsonConverter<object>>)cvtGenericMethod.Invoke(null, new object[] { this.CustomConverterType, this.PropertyInfo.PropertyType });
}
else
{
// Handle regular JsonConverter<T> types
var cvtGenericMethod = getConverterForTypeMethod.MakeGenericMethod(baseType, baseType.GenericTypeArguments[0]);
var converter = (JsonConverter<object>)cvtGenericMethod.Invoke(null, new object[] { this.CustomConverterType });
customConverter = _ => converter;
}
}

var defaultCvtGenericMethod = getDefaultConverterMethod.MakeGenericMethod(this.PropertyInfo.PropertyType);
var getDefaultConverter = (Func<JsonSerializerOptions, JsonConverter<object>>)defaultCvtGenericMethod.Invoke(null, new object[] { this.PropertyInfo.PropertyType });
this.getConverter = options => customConverter ?? getDefaultConverter(options);
this.getConverter = opts => customConverter != null ? customConverter(opts) : getDefaultConverter(opts);
}

return this.getConverter(options);
Expand Down Expand Up @@ -180,6 +189,20 @@ private static Func<JsonSerializerOptions, JsonConverter<object>> GetDefaultConv
{
return options => new JsonConverterAdapter<JsonConverter<TV>, TV>((JsonConverter<TV>)options.GetConverter(t));
}

private static Func<JsonSerializerOptions, JsonConverter<object>> GetConverterFromFactory<TV>(Type factoryType, Type propertyType)
{
return options =>
{
var conv = converterCache.GetOrAdd(propertyType, (key) =>
{
var factory = (JsonConverterFactory)Activator.CreateInstance(factoryType);
return factory.CreateConverter(propertyType, options);
});

return new JsonConverterAdapter<JsonConverter<TV>, TV>((JsonConverter<TV>)conv);
};
}
#pragma warning restore IDE0051 // Remove unused private members

}
Expand Down
Loading