Skip to content

Migration guide for v8 version upgrade

Olivier Bellone edited this page Mar 21, 2019 · 3 revisions

API version upgrade with stripe-java

Using stripe-java version 8 and above means that your API requests will behave as if you have upgraded to the pinned API version. To handle this change, the work required is actually the same as when you would do an actual API version upgrade on your account.

According to the official recommendations for Stripe API version upgrade, there are two main impact:

  1. Request and response for normal API calls
  2. Event response structure

1. Request and response for API calls

API calls, as discussed here, will refer to the associated methods on model classes that make HTTP requests, specifically excluding methods on Event.java to be discussed together with webhook events in the following section.

You can test a new version for the API calls by adding dependency of stripe-java version 8.0.0 and above. This will set Stripe-Version in the request header to the latest API version, simulating API calls as if you have upgraded.

API response

As any new stripe-java major versions, changes in schema of API responses are reflected explicitly in typed Java classes. With the compiler failing loudly, you can readily fix and update your getter methods. In version 8, fields or its corresponding getter methods previously marked with @Deprecated are removed, and other field changes are documented in the change log section below.

However, because this client library does not provide explicit enum for certain fields, your integration relying on string equality will not be caught by the compiler. If the enum value is removed or replaced with a different name, your code execution flow can be affected. Furthermore, the upgrade can change semantics of certain enums, and you should verify the implications on using the same enum values. For example, in 2017-06-05 version change, under_review now takes the meaning previously overloaded in other value for Account#Verification#getDisabledReason; if you use other to determine if the disabled account is under review, you should check for value under_review instead.

Account account = Account.retrieve();
// need to change "other" to "under_review" instead
if (account.getVerification().disabledReason.equals("other")) {
  // code path assuming account is still under review
}

Similar to enum is the value of object field in most of model classes. In 2018-09-24 version change the value for File is changed from file_upload to file. Explicit check for File#getObject should be updated accordingly.

Beyond the contract that fully-typed system can offer is the change in business implication, where the schema remains the same. This is less common, but does happen. For example in 2018-05-21 version change, an id of a subscription in line item lost its meaning as the subscription ID.

API requests

On the API requests, because this client library takes an untyped map as a parameter request to an API method, e.g., Source#create(Map<String, Object> params), parameters removed in an API upgrade will not be caught statically. For example, before 2018-08-23 version change Subscription#cancel allowed deferred cancellation with the at_period_end request parameter, but was removed in favor of Subscription#update with the cancel_at_period_end parameter. Similarly, there is extensive change in parameters for PaymentIntent#create and PaymentIntent#confirm for beta users of PaymentIntent in 2018-11-08 version change

Parameter validation is another common change. For example, 2018-10-31 version change requires Customer#update parameter name to be within 250 character-long. On the other hand, parameter validation can also become less restrictive, and the assumption you had on the valid returned response can be violated. For example, if you had relied on Stripe validation on uniqueness of SKU attributes during creation/update, and had built business logic around this assumption, after 2018-09-06 version change the validation is removed and the SKUs returned can have duplicate attributes. Your code depending on SKU attribute uniqueness has to be updated.

Parameters can also take new default values. Your old code may not explicitly pass the parameter value, but after version upgrade when default value kicks in, your same API call now has a different business implication. For example, in 2018-05-21 version change, Subscription#update will take trial_from_plan as true if it is not explicitly specified.

API errors

Less obvious ones are changes related to JSON error object now exposed at StripeException.getStripeError(). Different error types StripeError#type may be returned. If you are integrating to this "eum" type, check for version change like 2017-12-14 where new card_error is introduced.

Changes in HTTP error codes, however, can affect your integration as a different type of StripeException is thrown. For example, in 2016-10-19 version change, insufficient permissions will return code 403 instead of 401. This means your code checking for AuthenticationException may require updates to PermissionException instead. Similarly, 429 is introduced after 2015-09-08 version change instead of 400. As a result, any code handling rate-limit should be updated to check for RateLimitException.

Summary

Please consult the API change log for more details. A complementary recommendation for stripe-java is to pay close attention for the followings:

  • API response (beyond compiler complaints)
    • enum values represented as String,
    • object field values renaming
    • change in business meaning of the field
  • API request
    • parameter removal
    • parameter validation and effect on resulting API response
    • parameter default value
  • API error
    • HTTP error code and its corresponding StripeException

2. Event response structure

The official upgrade guide recommends configuring one test webhook to the latest API version. This allows you to test handling event and its event data object rendered at the upgraded API version. In the latest stripe-java, you will find an API version defined at its static value Stripe#API_VERSION matching the latest API version you can configure your test webhook to.

Alternatively, you can now create webhook endpoints with a specific API version corresponding to the pinned version of stripe-java. This gives you control of safe rollback window beyond just the default 72 hours, where you have two concurrent webhook endpoints receiving old and new versions of events respectively.

Safe webhook event deserialization when event API version matches the library's pinned API version

Given that webhook events have API version Event#apiVersion matching Stripe.API_VERSION, you can now safely use EventDataObjectDeserializer to get your data object.

Event event = Webhook.constructEvent(payload, signature, secret);
if (event.getType().equals("charge.succeeded")) {
   // Current, now deprecated
   Charge charge = (Charge) event.getData().getObject();
   // New returning value only when API versions matche and null otherwise.
   Charge charge = (Charge) event.getDataObjectDeserializer().getObject();
}

However, when API versions do not match because you are getting events from old webhook endpoints, or simply reading old events from Event#retrieve, there are compatibility helpers, motivated and explained in the following sections.

Motivation for handling event compatibility

The official guide also recommends the need for compatibility in handling events of both your current and new API versions. This is because for a short period of time after you upgrade, there will be in-flight events on old API version that you should still handle. Additionally, in the case of an API version roll-back, you will want to revert by changing the dashboard setting without the need to re-deploy your code. This is especially true if you have two webhook endpoints that you would like to switch the event read traffic seamlessly.

Actually, backward compatibility to read old events is needed in general for API request to read events going back to 30 days in Event#retrieve. One approach is having Java model classes that can deserialize events on both versions. This is what stripe-java version 7 and below attempts to do. In addition, when there is data type conflict (same field name but JSON has unexpected data type), custom deserialization and augmented fields are introduced. Version 8, however, will take a different approach.

Illustration of schema incompatibility failure

Currently, getting an Event will fail if the JSON schema for data object at event.data.object is not compatible with Java model class for its corresponding object type at event.data.object .object (the first object refers to the actual content we care about, and the second to type of object to be deserialized). Consider the JSON data below (fields omitted for illustration). It has event.data.object.object as invoice so Event#data#object will be deserialized to Invoice.java. Note the api_version before 2012-10-26 where the lines has become a common paginated collection structure with fields has_more, data, and total_count. Suppose the original lines schema is simply an array of invoice items. Such data type mismatch cannot be deserialized into recent Java model class, and Event.retrieve("evt_123") will simply fail.

{
  "id": "evt_123",
  "object": "event",
  "api_version": "2012-09-24",
  "created": 1549056424,
  "data": {
    "object": { // event data object of rendered at `api_version`
      "id": "in_1DwEmi2eZvKYlo2C5EbZEeaM",
      "object": "invoice", // tells deseiralizer this object should be `Invoice`
      "amount_due": 999,
      "amount_paid": 0,
      "lines": [ // not the paginated collection object as in recent API versions
        {
          "id": "sli_fde81ddce855c9",
          "object": "line_item",
          "amount": 999,
          "type": "subscription"
        }
      ],
      "status": "draft"
    }
  },
  "livemode": false,
  "pending_webhooks": 0,
  "request": {
    "id": null,
    "idempotency_key": null
  },
  "type": "invoice.updated"
}

Proxy object for deserialization in version 8

stripe-java version 8 now supporting model classes from only the pinned API version--and is not backward-compatible--provides a new abstraction to resolve the old event data incompatibility problem. A proxy object EventDataObjectDeserializer in Event#data#getDataObjectDeserializer is added. The general approach is to defer data object deserialization to your use case. This avoids always deserializing into model class, risking failure on incompatible schema. The interface makes explicit whether deserializiation to full model is safe, possible but giving lossy results, or failing with a checked exception. In addition, it provides a recovery method to handle incompatible JSON when necessary. This proxy object is introduced in favor of the now deprecated method EventData#getObject.

Event event = Event.retrieve("evt_123");
// deprecated method
EventData eventData = event.getData();
try {
  event.getData().getObject();
} catch (JsonParseException e) {
  // run-time exception on schema incompatible JSON
}
// instead use this proxy object
EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer();

Best-effort field getter method

In practice, you may only use a few fields from the event data object. When those fields are fully compatible with the current Java model, you should be able to access them, instead of failing because of some other incompatible legacy or malformed fields. The snippet below illustrates the first use case of a best-effort field getter. Suppose you want to get an invoice ID and status values only:

String invoiceId = null;
String status = null;
if (dataObjectDeserializer.deserialize()) {
  Invoice invoice = (Invoice) dataObjectDeserializer.getObject();
  invoiceId = invoice.getId();
  status = invoice.getStatus();
} else {
  try {
    Invoice invoice = (Invoice) dataObjectDeserializer.deserializeUnsafe();
    invoiceId = invoice.getId();
    status = invoice.getStatus();
  } catch (EventDataObjectDeserializationException e) {
    JsonElement parsed = new JsonParser().parse(e.getRawJson());
    invoiceId = parsed.getAsJsonObject().get("id").getAsString();
    status = parsed.getAsJsonObject().get("status").getAsString();
  }
}

The first control flow check dataObjectDeserializer.deserialize() determines whether it is safe for deserialization based on whether Event#apiVersion matches that of the library Stripe#API_VERSION. If so, you can use getObject() to get the full Stripe object Invoice here. Otherwise, getObject() simply returns null.

However, if there's an API version mismatch, but the schema is still compatible, you can use deserializeUnsafe() to get the Stripe object. It is unsafe because there is no guarantee on data completeness. When an API upgrade specifies field transformations like rename, the EventDataObjectDeserializer has no such knowledge. JSON with an old field, no longer represented in the new Java model class, will simply lose that field during deserialization.

Lastly, if the deserialization fails due to schema incompatibility, you can still access the raw JSON of the data object and access the relevant fields. Note that the unsafe deserialization throws a checked exception EventDataObjectDeserializationException, and raw JSON is accessible in the exception.

JSON compatibility transformation

The other use case is when you actually need a Stripe model as a result of event deserialization.
deserializeUnsafeWith(CompatibilityTransformer) provides a mechanism to transform the JSON to be compatible with the latest Java model class. Through CompatibilityTransformer, you can define which type of event and which version to apply the transformations to.

In the example below, you want to handle the same JSON invoice event created when you were on the Stripe API version before 2012-10-26. You can define an invoiceTransformer as a series of transformations required on an invoice object starting from the earliest API version (the event was created on).

EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer();
EventDataObjectDeserializer.CompatibilityTransformer invoiceTransformer =
    new com.stripe.model.EventDataObjectDeserializer.CompatibilityTransformer() {
      @Override
      public JsonObject transform(JsonObject rawJsonObject, String apiVersion,
                                  String eventType) {
        // Attempt to be forward compatible only for invoice events
        if (!eventType.startsWith("invoice.")) {
          return rawJsonObject;
        }
        
        // update guide on Invoice: https://stripe.com/docs/upgrades#2012-10-26
        // `lines` for line items are in the form of paginated collection
        // supposed the old schema for `lines` is a JSON array
        if (versionToDate(apiVersion).before(versionToDate("2012-10-26"))) {
          JsonArray lines = rawJsonObject.get("lines").getAsJsonArray();
          JsonObject paginatedLines = new JsonObject();
          paginatedLines.add("data", lines);
          paginatedLines.add("has_more", new JsonPrimitive(false));
          paginatedLines.add("total_count", new JsonPrimitive(lines.size()));
          rawJsonObject.add("lines", paginatedLines);
        }
        
        // update guide on Invoice: https://stripe.com/docs/upgrades#2017-12-14
        // invoice item description is always set, if you downstream application
        // code expects non-null description, consider adding default values
        if (versionToDate(apiVersion).before(versionToDate("2017-12-14"))) {
          // add you default values to the line item
        }
        
        // update guide on Invoice: https://stripe.com/docs/upgrades#2018-05-21
        // change in semantics of `id` for invoice line items, but schema remains the same
    
        // update guide on Invoice: https://stripe.com/docs/upgrades#2018-10-31
        // billing_reason taking a new enum, but the old enum is still valid, so no change.

        // following update guide on Invoice: https://stripe.com/docs/upgrades#2018-11-08
        if (versionToDate(apiVersion).before(versionToDate("2018-11-08"))) {
          // From change log, `closed` is deprecated in favor of `auto_advance`
          // when `closed` is true, `auto_advance` is false.
          boolean closedValue = rawJsonObject.get("closed").getAsBoolean();
          rawJsonObject.add("auto_advance", new JsonPrimitive(!closedValue));

          // From change log, `forgiven` is deprecated in favor of `uncollectible` status.
          if (rawJsonObject.get("forgiven").getAsBoolean()) {
            rawJsonObject.add("status", new JsonPrimitive("uncollectible"));
          }
        }
        return rawJsonObject;
      }
    };
StripeObject deserialized = dataObjectDeserializer.deserializeUnsafeWith(invoiceTransformer);
Invoice invoice = (Invoice) deserialized;

Because eventType is namespaced, you can filter out the class of object to handle. The apiVersion helps to distinguish events that are eligible for certain transformations. Specifically, invoice event with its API version before these dates needs the following transformation in order:

  • 2012-10-26 - converts two nested list into the paginated object structure
  • 2017-12-14 - sets default description to the now always-present field in invoice item
  • 2018-11-08 - maps value of deprecated fields to a newly introduced one

The transformation ensures that schema is compatible and avoids deserialization failure. It also allows custom default values for fields that are now non-optional, and does value-mapping to preserve the semantics of the deserailized object. Using deserializeUnsafeWith with a specified transformer, you can recover model objects, from old event versions, to be more consistent with objects from the later ones.

Deprecating event old version handling

After the default 72 hours when you have successfully upgraded the API version, any pending old webhook events would have been delivered and handled in application code. After 30 days, events guaranteed to be available will all have been created with the upgraded API version. At this time, you can safely deprecate the backward compatibility event handling logic; model classes from API calls, and in event data object via webhook or event endpoints will be of the new API version.