braintree / braintree_dotnet

Braintree .NET library
https://developer.paypal.com/braintree/docs/start/overview
MIT License
136 stars 73 forks source link

Enumeration types make serialization/deserialization difficult #60

Closed top2tech closed 7 years ago

top2tech commented 7 years ago

There are many (about 27) Enumeration types in the Braintree .net client library. Each of them needs to write a custom serialization/deserialization logic. e.g. Various Status(e.g. SubscriptionStatus, TransactionStatus)

I understand the reason is .net enum is serialized to integer and Braintree Api wants string. But there are two ways to solve this enum-as-string problem:

  1. Use NewtonSoft.json, the de facto .net standard for serialization, which has built-in StringEnumConverter to serialize enum as string.

  2. Use plain string, e.g.:

    public class TransactionStatus
    {
        public static readonly string AUTHORIZATION_EXPIRED ="authorization_expired";
        public static readonly string AUTHORIZED = "authorized";
        public static readonly string AUTHORIZING = "authorizing";
    ...}

Build and maintain 27 custom serialization/deserialization logic is a daunting task, and may not work in some scenarios.

sdcoffey commented 7 years ago

Hey @top2tech,

Thanks for your suggestion, and sorry for the delay in responding. All of your assumptions are correct, we do add custom serialization/deserialization logic for all of these enums because of the way they're represented under the hood by the .NET runtime. We do it this way because all of our requests and responses to the Braintree Gateway are serialized as XML, which rules out using NewtonSoft.Json, and we want developers to have the conveniences of enums, instead of using Strings, which don't afford the same level of restriction of possible values. I'm going to close this for now, but if you feel that I haven't addressed your suggestion completely, or if you wish to discuss further, please feel free to reopen this issue at any time. Thanks!

simeyla commented 5 years ago

@sdcoffey This is such a pain for early development where I just want to look at the whole incoming message. If I serialize it then it just becomes a huge mess with all the values disappearing.

WHY can't you expose a RawMessage property? It would make deciding which fields are useful to me a much easier task. If I have a response that I can't handle then I want to log it.

simeyla commented 5 years ago

Ok while I think complaining about this is still legitimate (you guys are a bank and by not making it easy for developers to see the full response easily people WILL make mistakes) I think it's more helpful to provide a solution:

You can quite easily use a custom serializer with JSON.NET - fortunately all enumerations extend Braintree.Enumeration.

    using BT = Braintree;

    public class BraintreeEnumConverter : JsonConverter
    {
        private static Type[] _types;

        public BraintreeEnumConverter()
        {
            if (_types == null)
            {
                _types = typeof(BT.Enumeration).Assembly.DefinedTypes.Where(x => x.BaseType == typeof(BT.Enumeration)).ToArray();
            }
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            JToken t = JToken.FromObject(value);

            if (t.Type != JTokenType.Object)
            {
                t.WriteTo(writer);
            }
            else
            {
                JObject o = (JObject)t;

                o.AddFirst(new JProperty("type", value.GetType().Name));
                o.AddFirst(new JProperty("value", (value as BT.Enumeration).ToString()));    // why is Name protected???

                o.WriteTo(writer);
            }
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
        }

        public override bool CanRead
        {
            get { return false; }
        }

        public override bool CanConvert(Type objectType)
        {
            return _types.Any(t => t == objectType);
        }
    }

You run it like this:

        var serialized = JsonConvert.SerializeObject(saleResult, Formatting.Indented, new BraintreeEnumConverter());

Note: I chose to include the enumeration type and value, you can adjust this as needed - or create two converters for different purposes.

Here's a sample:

  "Type": {
      "value": "sale",
      "type": "TransactionType"
    },
    "UpdatedAt": "2019-02-01T19:26:52Z",
    "CustomFields": {},
    "ServiceFeeAmount": null,
    "DisbursementDetails": {
      "SettlementAmount": null,
      "SettlementCurrencyIsoCode": null,
      "SettlementCurrencyExchangeRate": null,
      "FundsHeld": null,
      "Success": null,
      "DisbursementDate": null
    },
    "ApplePayDetails": null,
    "AndroidPayDetails": null,
    "AmexExpressCheckoutDetails": null,
    "PayPalDetails": null,
    "CoinbaseDetails": null,
    "VenmoAccountDetails": null,
    "UsBankAccountDetails": null,
    "IdealPaymentDetails": null,
    "VisaCheckoutCardDetails": null,
    "MasterpassCardDetails": null,
    "SamsungPayCardDetails": null,
    "PaymentInstrumentType": {
      "value": "credit_card",
      "type": "PaymentInstrumentType"
    },
    "RiskData": null,
    "ThreeDSecureInfo": null,
    "FacilitatedDetails": null,
    "FacilitatorDetails": null,
    "DiscountAmount": null,
    "ShippingAmount": null,
    "ShipsFromPostalCode": null,
    "NetworkTransactionId": "020190201192652",
    "AuthorizationExpiresAt": "2019-02-08T19:26:52Z"
  }