Skip to content
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# CHANGELOG

## v9.0.0 (Unreleased)

- Breaking: API resource responses are now plain JSON-compatible objects rather than model class instances

## v8.9.0 (2026-06-25)

- Adds `params` to `requestPin` ensuring users can pass `easypost_details` to the call.
Expand Down
22 changes: 22 additions & 0 deletions UPGRADE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,33 @@
Use the following guide to assist in the upgrade process of the `easypost-node` library between major versions.

- [Upgrading from 7.x to 8.0](#upgrading-from-7x-to-80)
- [Upgrading from 8.x to 9.0](#upgrading-from-8x-to-90)
- [Upgrading from 6.x to 7.0](#upgrading-from-6x-to-70)
- [Upgrading from 5.x to 6.0](#upgrading-from-5x-to-60)
- [Upgrading from 4.x to 5.0](#upgrading-from-4x-to-50)
- [Upgrading from 3.x to 4.0](#upgrading-from-3x-to-40)

## Upgrading from 8.x to 9.0

### 9.0 High Impact Changes

- [Response Objects Are Now Plain JSON Objects](#90-response-objects-are-now-plain-json-objects)

### 9.0 Response Objects Are Now Plain JSON Objects

Likelihood of Impact: **High**

API responses are now returned as plain JSON-compatible objects instead of model class instances.

Instance helper methods such as `shipment.lowestRate()` remain available on returned objects:

```javascript
const shipment = await client.Shipment.create({ ... });
const boughtShipment = await client.Shipment.buy(shipment.id, shipment.lowestRate());
```

This change improves compatibility with serializers and SSR frameworks that require plain objects.

## Upgrading from 7.x to 8.0

### 8.0 High Impact Changes
Expand Down
14 changes: 14 additions & 0 deletions src/models/easypost_object.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,18 @@ export default class EasyPostObject {
static created_at;
static updated_at;
static _params;

static [Symbol.hasInstance](instance) {
if (instance == null || typeof instance !== 'object') {
return false;
}

const modelConstructor = instance[Symbol.for('easypost.modelConstructor')];
if (typeof modelConstructor === 'function') {
return modelConstructor === this;
}

// Fallback for plain API payloads that include object names like "Address" or "Shipment".
return typeof instance.object === 'string' && instance.object === this.name;
}
}
76 changes: 71 additions & 5 deletions src/services/base_service.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import EndOfPaginationError from '../errors/general/end_of_pagination_error';
import Address from '../models/address';
import ApiKey from '../models/api_key';
import Batch from '../models/batch';
Expand Down Expand Up @@ -26,7 +27,8 @@ import Shipment from '../models/shipment';
import Tracker from '../models/tracker';
import User from '../models/user';
import Webhook from '../models/webhook';
import EndOfPaginationError from '../errors/general/end_of_pagination_error';

const EASYPOST_MODEL_CONSTRUCTOR_TAG = Symbol.for('easypost.modelConstructor');

/**
* A map of EasyPost object ID prefixes to their associated class names.
Expand Down Expand Up @@ -103,18 +105,68 @@ export default (easypostClient) =>
* @param {EasyPostClient} easypostClient The {@link EasyPostClient} instance to use for API calls.
*/
class BaseService {
/**
* Converts model instances and nested data into plain JSON-compatible objects while
* preserving model helper methods as non-enumerable properties.
* @internal
* @param {*} response The value to serialize.
* @returns {*} A plain object/array/scalar.
*/
static _toPlainEasyPostObject(response) {
if (Array.isArray(response)) {
return response.map((value) => this._toPlainEasyPostObject(value));
}

if (typeof response === 'object' && response !== null) {
const plainObject = {};
const prototype = Object.getPrototypeOf(response);

if (prototype && prototype !== Object.prototype) {
Object.defineProperty(plainObject, EASYPOST_MODEL_CONSTRUCTOR_TAG, {
value: response.constructor,
enumerable: false,
configurable: true,
writable: false,
});

const descriptors = Object.getOwnPropertyDescriptors(prototype);

Object.entries(descriptors).forEach(([name, descriptor]) => {
if (name === 'constructor' || typeof descriptor.value !== 'function') {
return;
}

Object.defineProperty(plainObject, name, {
value: descriptor.value.bind(plainObject),
enumerable: false,
configurable: true,
writable: true,
});
});
}

Object.keys(response).forEach((key) => {
plainObject[key] = this._toPlainEasyPostObject(response[key]);
});

return plainObject;
}

return response;
}

/**
* Converts a JSON response and all its nested elements to associated {@link EasyPostObject}-based class instances.
* @internal
* @param {*} response The JSON response to convert (usually a `Map` or `Array`).
* @param {*} params The parameters passed when fetching the response
* @param {*} params The parameters passed when fetching the response.
* @returns {*} An {@link EasyPostObject}-based class instance or an `Array` of {@link EasyPostObject}-based class instances.
*/
static _convertToEasyPostObject(response, params) {
static _buildEasyPostObject(response, params) {
if (Array.isArray(response)) {
return response.map((value) => {
if (typeof value === 'object') {
return this._convertToEasyPostObject(value, params);
return this._buildEasyPostObject(value, params);
}
return value;
});
Expand All @@ -137,16 +189,30 @@ export default (easypostClient) =>
}

Object.keys(response).forEach((key) => {
classObject[key] = this._convertToEasyPostObject(response[key], params);
classObject[key] = this._buildEasyPostObject(response[key], params);
});

classObject._params = params;

return classObject;
}

return response;
}

/**
* Converts a JSON response to plain JSON-compatible output while preserving model-based conversion internally.
* @internal
* @param {*} response The JSON response to convert (usually a `Map` or `Array`).
* @param {*} params The parameters passed when fetching the response.
* @returns {*} A plain object or array suitable for JSON serialization.
*/
static _convertToEasyPostObject(response, params) {
const modelResponse = this._buildEasyPostObject(response, params);

return this._toPlainEasyPostObject(modelResponse);
}

/**
* Creates an EasyPost Object via the API.
* @internal
Expand Down
Loading