Skip to content

Commit

Permalink
Fix deserialization logic for polymorphic types
Browse files Browse the repository at this point in the history
  • Loading branch information
ob-stripe committed Nov 28, 2018
1 parent bd46326 commit d8c06a2
Show file tree
Hide file tree
Showing 25 changed files with 388 additions and 296 deletions.
1 change: 1 addition & 0 deletions src/Stripe.net/Entities/Charges/Charge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ internal object InternalReview
/// For most Stripe users, the source of every charge is a credit or debit card. This hash is then the card object describing that card.
/// </summary>
[JsonProperty("source")]
[JsonConverter(typeof(StripeObjectConverter))]
public IPaymentSource Source { get; set; }

#region Expandable Transfer
Expand Down
2 changes: 2 additions & 0 deletions src/Stripe.net/Entities/Events/EventData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ namespace Stripe
{
using System.Collections.Generic;
using Newtonsoft.Json;
using Stripe.Infrastructure;

public class EventData : StripeEntity
{
[JsonProperty("object")]
[JsonConverter(typeof(StripeObjectConverter))]
public IHasObject Object { get; set; }

[JsonProperty("previous_attributes")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
namespace Stripe
{
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Stripe.Infrastructure;

public class PaymentIntentLastPaymentError : StripeEntity
Expand All @@ -26,6 +24,7 @@ public class PaymentIntentLastPaymentError : StripeEntity
public string Param { get; set; }

[JsonProperty("source")]
[JsonConverter(typeof(StripeObjectConverter))]
public IPaymentSource Source { get; set; }

[JsonProperty("type")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
namespace Stripe
{
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Stripe.Infrastructure;

public class PaymentIntentSourceAction : StripeEntity
{
Expand Down
3 changes: 2 additions & 1 deletion src/Stripe.net/Entities/StripeList.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ namespace Stripe
using System.Collections;
using System.Collections.Generic;
using Newtonsoft.Json;
using Stripe.Infrastructure;

[JsonObject]
public class StripeList<T> : StripeEntity, IHasObject, IEnumerable<T>
{
[JsonProperty("object")]
public string Object { get; set; }

[JsonProperty("data")]
[JsonProperty("data", ItemConverterType = typeof(StripeObjectConverter))]
public List<T> Data { get; set; }

/// <summary>
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace Stripe.Infrastructure
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

internal class DateTimeConverter : DateTimeConverterBase
public class DateTimeConverter : DateTimeConverterBase
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
Expand Down

This file was deleted.

This file was deleted.

140 changes: 73 additions & 67 deletions src/Stripe.net/Infrastructure/JsonConverters/StripeObjectConverter.cs
Original file line number Diff line number Diff line change
@@ -1,77 +1,83 @@
namespace Stripe.Infrastructure
{
using System;
using System.Collections.Generic;
using System.Reflection;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

/// <summary>
/// This converter is used to deserialize attributes declared as `IHasObject` into concrete
/// model classes. In other words, this converter can deserialize any Stripe object based on
/// the value of the <c>object</c> attribute. This is useful when the type of the object has
/// to be determined at runtime, such as when decoding the <c>data.object</c> attribute of
/// event objects.
/// This converter can be used to deserialize any Stripe object. It is mainly useful for
/// polymorphic attributes, when the property's type is an interface instead of a concrete type.
/// In this case, the converter will use the value of the `object` key in the JSON payload to
/// decide which concrete type to instantiate. If no concrete type is found (or if one is found,
/// but it's not compatible with the expected interface), then the converter returns `null`.
/// </summary>
internal class StripeObjectConverter : AbstractStripeObjectConverter<IHasObject>
public class StripeObjectConverter : JsonConverter
{
protected override Dictionary<string, Func<string, IHasObject>> ObjectsToMapperFuncs
=> new Dictionary<string, Func<string, IHasObject>>()
/// <summary>
/// Gets a value indicating whether this <see cref="JsonConverter"/> can write JSON.
/// </summary>
/// <value>
/// <c>true</c> if this <see cref="JsonConverter"/> can write JSON; otherwise, <c>false</c>.
/// </value>
public override bool CanWrite => false;

/// <summary>
/// Writes the JSON representation of the object.
/// </summary>
/// <param name="writer">The <see cref="JsonWriter"/> to write to.</param>
/// <param name="value">The value.</param>
/// <param name="serializer">The calling serializer.</param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException("StripeObjectConverter should only be used while deserializing.");
}

/// <summary>
/// Reads the JSON representation of the object.
/// </summary>
/// <param name="reader">The <see cref="JsonReader"/> to read from.</param>
/// <param name="objectType">Type of the object.</param>
/// <param name="existingValue">The existing value of object being read.</param>
/// <param name="serializer">The calling serializer.</param>
/// <returns>The object value.</returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}

var jsonObject = JObject.Load(reader);
var objectValue = (string)jsonObject["object"];

Type concreteType = Util.GetConcreteType(objectType, objectValue);
if (concreteType == null)
{
// Couldn't find a concrete type to instantiate, return null.
return null;
}

var value = Activator.CreateInstance(concreteType);

using (var subReader = jsonObject.CreateReader())
{
serializer.Populate(subReader, value);
}

return value;
}

/// <summary>
/// Determines whether this instance can convert the specified object type.
/// </summary>
/// <param name="objectType">Type of the object.</param>
/// <returns>
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
/// </returns>
public override bool CanConvert(Type objectType)
{
{ "account", Mapper<Account>.MapFromJson },
{ "apple_pay_domain", Mapper<ApplePayDomain>.MapFromJson },
{ "application_fee", Mapper<ApplicationFee>.MapFromJson },
{ "balance", Mapper<Balance>.MapFromJson },
{ "balance_transaction", Mapper<BalanceTransaction>.MapFromJson },
{ "bank_account", Mapper<BankAccount>.MapFromJson },
{ "card", Mapper<Card>.MapFromJson },
{ "charge", Mapper<Charge>.MapFromJson },
{ "country_spec", Mapper<CountrySpec>.MapFromJson },
{ "coupon", Mapper<Coupon>.MapFromJson },
{ "customer", Mapper<Customer>.MapFromJson },
{ "discount", Mapper<Discount>.MapFromJson },
{ "dispute", Mapper<Dispute>.MapFromJson },
{ "ephemeral_key", Mapper<EphemeralKey>.MapFromJson },
{ "event", Mapper<Event>.MapFromJson },
{ "exchange_rate", Mapper<ExchangeRate>.MapFromJson },
{ "fee_refund", Mapper<ApplicationFeeRefund>.MapFromJson },
{ "file", Mapper<File>.MapFromJson },
{ "file_link", Mapper<FileLink>.MapFromJson },
{ "invoice", Mapper<Invoice>.MapFromJson },
{ "invoiceitem", Mapper<InvoiceItem>.MapFromJson },
{ "issuing.authorization", Mapper<Issuing.Authorization>.MapFromJson },
{ "issuing.cardholder", Mapper<Issuing.Cardholder>.MapFromJson },
{ "issuing.card", Mapper<Issuing.Card>.MapFromJson },
{ "issuing.dispute", Mapper<Issuing.Dispute>.MapFromJson },
{ "issuing.transaction", Mapper<Issuing.Transaction>.MapFromJson },
{ "login_link", Mapper<LoginLink>.MapFromJson },
{ "order", Mapper<Order>.MapFromJson },
{ "order_item", Mapper<OrderItem>.MapFromJson },
{ "order_return", Mapper<OrderReturn>.MapFromJson },
{ "payment_intent", Mapper<PaymentIntent>.MapFromJson },
{ "payout", Mapper<Payout>.MapFromJson },
{ "plan", Mapper<Plan>.MapFromJson },
{ "product", Mapper<Product>.MapFromJson },
{ "radar.value_list", Mapper<Radar.ValueList>.MapFromJson },
{ "radar.value_list_item", Mapper<Radar.ValueListItem>.MapFromJson },
{ "recipient", Mapper<Recipient>.MapFromJson },
{ "refund", Mapper<Refund>.MapFromJson },
{ "reporting.report_run", Mapper<Reporting.ReportRun>.MapFromJson },
{ "reporting.report_type", Mapper<Reporting.ReportType>.MapFromJson },
{ "scheduled_query_run", Mapper<Sigma.ScheduledQueryRun>.MapFromJson },
{ "sku", Mapper<Sku>.MapFromJson },
{ "source", Mapper<Source>.MapFromJson },
{ "source_mandate_notification", Mapper<SourceMandateNotification>.MapFromJson },
{ "source_transaction", Mapper<SourceTransaction>.MapFromJson },
{ "subscription", Mapper<Subscription>.MapFromJson },
{ "subscription_item", Mapper<SubscriptionItem>.MapFromJson },
{ "terminal.connection_token", Mapper<Terminal.ConnectionToken>.MapFromJson },
{ "terminal.location", Mapper<Terminal.Location>.MapFromJson },
{ "terminal.reader", Mapper<Terminal.Reader>.MapFromJson },
{ "three_d_secure", Mapper<ThreeDSecure>.MapFromJson },
{ "token", Mapper<Token>.MapFromJson },
{ "topup", Mapper<Topup>.MapFromJson },
{ "transfer", Mapper<Transfer>.MapFromJson },
{ "transfer_reversal", Mapper<TransferReversal>.MapFromJson },
{ "usage_record", Mapper<UsageRecord>.MapFromJson },
{ "usage_record_summary", Mapper<UsageRecordSummary>.MapFromJson },
};
return objectType.GetTypeInfo().IsInterface;
}
}
}
22 changes: 14 additions & 8 deletions src/Stripe.net/Infrastructure/Public/Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,7 @@ namespace Stripe

public static class Mapper<T>
{
private static JsonConverter[] converters =
{
new BalanceTransactionSourceConverter(),
new ExternalAccountConverter(),
new PaymentSourceConverter(),
new StripeObjectConverter(),
};
public static JsonSerializerSettings SerializerSettings = InitSerializerSettings();

public static List<T> MapCollectionFromJson(string json, string token = "data", StripeResponse stripeResponse = null)
{
Expand Down Expand Up @@ -43,7 +37,7 @@ public static T MapFromJson(string json, string parentToken = null, StripeRespon
{
var jsonToParse = string.IsNullOrEmpty(parentToken) ? json : JObject.Parse(json).SelectToken(parentToken).ToString();

var result = JsonConvert.DeserializeObject<T>(jsonToParse, converters);
var result = JsonConvert.DeserializeObject<T>(jsonToParse, SerializerSettings);

// if necessary, we might need to apply the stripe response to nested properties for StripeList<T>
ApplyStripeResponse(json, stripeResponse, result);
Expand Down Expand Up @@ -73,5 +67,17 @@ private static void ApplyStripeResponse(string json, StripeResponse stripeRespon

stripeResponse.ObjectJson = json;
}

private static JsonSerializerSettings InitSerializerSettings()
{
return new JsonSerializerSettings
{
Converters = new List<JsonConverter>
{
new StripeObjectConverter(),
},
DateParseHandling = DateParseHandling.None,
};
}
}
}
14 changes: 8 additions & 6 deletions src/Stripe.net/Infrastructure/StringOrObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ public static void Map(object value, Action<string> updateId, Action<T> updateOb
{
if (value is JObject)
{
// We reserialize the JObject instance to a string so we can pass it to the Mapper
// class. This ensures our custom deserialization / converters are used even when
// deserializing expanded fields.
// TODO: We could probably avoid the unnecessary deserialization+reserialization
// with some refactoring.
T item = Mapper<T>.MapFromJson(value.ToString());
var item = default(T);
string objectValue = ((JObject)value).SelectToken("object")?.ToString();
Type concreteType = Util.GetConcreteType(typeof(T), objectValue);

if (concreteType != null)
{
item = (T)((JToken)value).ToObject(concreteType);
}

if (item != null)
{
Expand Down
Loading

0 comments on commit d8c06a2

Please sign in to comment.