Skip to content
Draft
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,62 @@ namespace Stripe.Infrastructure
using System.Text.Json;
using System.Text.Json.Serialization;

/// <summary>
/// A JsonConverterFactory for use with DateTime and DateTime? implementations
/// to ensure we return a correctly typed custom converter.
/// </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
{
public override bool CanConvert(Type typeToConvert)
{
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
}

public override JsonConverter CreateConverter(
Type type,
JsonSerializerOptions options)
{
if (type == typeof(DateTime?))
{
return new STJUnixNullableDateTimeConverterImpl();
}
else
{
return new STJUnixDateTimeConverterImpl();
}
}
}

#pragma warning disable SA1402 // File may only contain a single type
internal class STJUnixNullableDateTimeConverterImpl : JsonConverter<DateTime?>
#pragma warning restore SA1402 // File may only contain a single type
{
private static readonly STJUnixDateTimeConverterImpl BaseConverter = new STJUnixDateTimeConverterImpl();

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);
}

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 +69,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 Down
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 = options => default(JsonConverter<object>);
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))
{
var cvtGenericMethod = getConverterFromFactoryMethod.MakeGenericMethod(this.PropertyInfo.PropertyType);
customConverter = (Func<JsonSerializerOptions, JsonConverter<object>>)cvtGenericMethod.Invoke(null, new object[] { this.CustomConverterType, this.PropertyInfo.PropertyType });
}
else
{
var cvtGenericMethod = getConverterForTypeMethod.MakeGenericMethod(baseType, baseType.GenericTypeArguments[0]);
customConverter = _ => (JsonConverter<object>)cvtGenericMethod.Invoke(null, new object[] { this.CustomConverterType });
}
}

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 = options => customConverter(options) ?? getDefaultConverter(options);
}

return this.getConverter(options);
Expand Down Expand Up @@ -176,6 +185,20 @@ private static JsonConverter<object> GetConverterForType<T, TV>(Type ct)
return new JsonConverterAdapter<T, TV>((T)conv);
}

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

return new JsonConverterAdapter<JsonConverter<TV>, TV>((JsonConverter<TV>)conv);
};
}

private static Func<JsonSerializerOptions, JsonConverter<object>> GetDefaultConverter<TV>(Type t)
{
return options => new JsonConverterAdapter<JsonConverter<TV>, TV>((JsonConverter<TV>)options.GetConverter(t));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#if NET6_0_OR_GREATER
namespace StripeTests
{
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Stripe;
using Stripe.Infrastructure;
using Xunit;
using STJS = System.Text.Json.Serialization;

public class STJUnixDateTimeConverterTest : BaseStripeTest
{
private readonly STJUnixDateTimeConverter converter;
private readonly JsonSerializerOptions options;

public STJUnixDateTimeConverterTest()
{
this.converter = new STJUnixDateTimeConverter();
this.options = new JsonSerializerOptions();
}

[Fact]
public void Read_ValidUnixTimestampAsNumber()
{
var json = "{\n \"created_at\": 1640995200\n}";
var obj = JsonSerializer.Deserialize<TestObject>(json);

Assert.Equal(1640995200, obj.CreatedAt);
Assert.NotNull(obj.CreatedAt);
}

[Fact]
public void Read_ValidNullUnixTimestamp()
{
var payload = "{\n \"id\": \"evt_1Rr1JvFtG20dLAMsr7rHdgc2\",\n \"object\": \"event\",\n \"account\": \"acct_1RmfQsFtG20dLAMs\",\n \"api_version\": \"2025-07-30.basil\",\n \"context\": \"acct_1RmfQsFtG20dLAMs\",\n \"created\": 1753986915,\n \"data\": {\n \"object\": {\n \"id\": \"sub_1Rr1JuFtG20dLAMseqyV6s6G\",\n \"object\": \"subscription\",\n \"application\": \"ca_SmZ5iAGaRQDDNPqhbcsigRQREqD0dLt1\",\n \"application_fee_percent\": null,\n \"automatic_tax\": {\n \"disabled_reason\": null,\n \"enabled\": false,\n \"liability\": null\n },\n \"billing_cycle_anchor\": 1753986914,\n \"billing_cycle_anchor_config\": null,\n \"billing_mode\": {\n \"type\": \"classic\",\n \"updated_at\": 0\n },\n \"billing_thresholds\": null,\n \"cancel_at\": null,\n \"cancel_at_period_end\": false,\n \"canceled_at\": null,\n \"cancellation_details\": {\n \"comment\": null,\n \"feedback\": null,\n \"reason\": null\n },\n \"collection_method\": \"charge_automatically\",\n \"created\": 1753986914,\n \"currency\": \"usd\",\n \"customer\": \"cus_SmZKVtb338KqvK\",\n \"days_until_due\": null,\n \"default_payment_method\": \"pm_1Rr0QcFtG20dLAMsU9KmfBDC\",\n \"default_source\": null,\n \"default_tax_rates\": [],\n \"description\": null,\n \"discounts\": [],\n \"ended_at\": null,\n \"invoice_settings\": {\n \"account_tax_ids\": null,\n \"issuer\": {\n \"account\": null,\n \"type\": \"self\"\n }\n },\n \"items\": {\n \"object\": \"list\",\n \"data\": [\n {\n \"id\": \"si_SmaUvBvBsIYUlI\",\n \"object\": \"subscription_item\",\n \"billing_thresholds\": null,\n \"created\": 1753986915,\n \"current_period_end\": 1756665314,\n \"current_period_start\": 1753986914,\n \"discounts\": [],\n \"metadata\": {},\n \"plan\": {\n \"id\": \"price_1Rr0BmFtG20dLAMsgqSmxAvZ\",\n \"object\": \"plan\",\n \"active\": true,\n \"amount\": 1167,\n \"amount_decimal\": 1167,\n \"billing_scheme\": \"per_unit\",\n \"created\": 1753982566,\n \"currency\": \"usd\",\n \"interval\": \"month\",\n \"interval_count\": 1,\n \"livemode\": false,\n \"metadata\": {\n \"3c_round_up\": \"0\",\n \"3c_zone_id\": \"1\",\n \"3c_agreement_id\": \"5\"\n },\n \"meter\": null,\n \"nickname\": \"Zone 1\",\n \"product\": \"3c_agreement_5\",\n \"tiers\": null,\n \"tiers_mode\": null,\n \"transform_usage\": null,\n \"trial_period_days\": null,\n \"usage_type\": \"licensed\"\n },\n \"price\": {\n \"id\": \"price_1Rr0BmFtG20dLAMsgqSmxAvZ\",\n \"object\": \"price\",\n \"active\": true,\n \"billing_scheme\": \"per_unit\",\n \"created\": 1753982566,\n \"currency\": \"usd\",\n \"currency_options\": null,\n \"custom_unit_amount\": null,\n \"livemode\": false,\n \"lookup_key\": null,\n \"metadata\": {\n \"3c_round_up\": \"0\",\n \"3c_zone_id\": \"1\",\n \"3c_agreement_id\": \"5\"\n },\n \"nickname\": \"Zone 1\",\n \"product\": \"3c_agreement_5\",\n \"recurring\": {\n \"interval\": \"month\",\n \"interval_count\": 1,\n \"meter\": null,\n \"trial_period_days\": null,\n \"usage_type\": \"licensed\"\n },\n \"tax_behavior\": \"unspecified\",\n \"tiers\": null,\n \"tiers_mode\": null,\n \"transform_quantity\": null,\n \"type\": \"recurring\",\n \"unit_amount\": 1167,\n \"unit_amount_decimal\": 1167\n },\n \"quantity\": 1,\n \"subscription\": \"sub_1Rr1JuFtG20dLAMseqyV6s6G\",\n \"tax_rates\": []\n }\n ],\n \"has_more\": false,\n \"url\": \"/v1/subscription_items?subscription=sub_1Rr1JuFtG20dLAMseqyV6s6G\"\n },\n \"latest_invoice\": \"in_1Rr1JvFtG20dLAMsdm4DSlKg\",\n \"livemode\": false,\n \"metadata\": {},\n \"next_pending_invoice_item_invoice\": null,\n \"on_behalf_of\": null,\n \"pause_collection\": null,\n \"payment_settings\": {\n \"payment_method_options\": null,\n \"payment_method_types\": null,\n \"save_default_payment_method\": null\n },\n \"pending_invoice_item_interval\": null,\n \"pending_setup_intent\": null,\n \"pending_update\": null,\n \"schedule\": \"sub_sched_1Rr1JuFtG20dLAMsgegakr2q\",\n \"start_date\": 1753986914,\n \"status\": \"active\",\n \"test_clock\": null,\n \"transfer_data\": null,\n \"trial_end\": null,\n \"trial_settings\": {\n \"end_behavior\": {\n \"missing_payment_method\": \"create_invoice\"\n }\n },\n \"trial_start\": null\n },\n \"previous_attributes\": null\n },\n \"livemode\": false,\n \"pending_webhooks\": 4,\n \"request\": {\n \"id\": \"req_MKH4UX85vdDnq3\",\n \"idempotency_key\": \"caaec5d2-bf4d-4009-ab91-316a2e5394c4:283c3507-60c3-51e5-3df5-8b6f15f37ff6\"\n },\n \"type\": \"customer.subscription.created\"\n}";

// Fails
var des_json = JsonSerializer.Deserialize<Stripe.Event>(payload);
}

private class TestObject : StripeEntity<TestObject>
{
[STJS.JsonPropertyName("created_at")]
public long? CreatedAt { get; set; }
}
}
}
#endif
Loading