Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix deserialization logic for polymorphic types #1396

Merged
merged 1 commit into from
Dec 11, 2018
Merged
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
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 = StripeTypeRegistry.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,
ob-stripe marked this conversation as resolved.
Show resolved Hide resolved
};
}
}
}
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 = StripeTypeRegistry.GetConcreteType(typeof(T), objectValue);

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

if (item != null)
{
Expand Down
Loading