From 8f524912d9ca7e112ea54ca0ba6472b64f2faef7 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Tue, 28 Jul 2026 12:19:17 -0600 Subject: [PATCH] !feat: json responses instead of model class instances --- CHANGELOG.md | 4 ++ UPGRADE_GUIDE.md | 22 ++++++++++ src/models/easypost_object.js | 14 +++++++ src/services/base_service.js | 76 ++++++++++++++++++++++++++++++++--- 4 files changed, 111 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8d4f6d43..35818d405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/UPGRADE_GUIDE.md b/UPGRADE_GUIDE.md index e90a3f2dc..b64291e4c 100644 --- a/UPGRADE_GUIDE.md +++ b/UPGRADE_GUIDE.md @@ -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 diff --git a/src/models/easypost_object.js b/src/models/easypost_object.js index d84435fb6..deb10eb4a 100644 --- a/src/models/easypost_object.js +++ b/src/models/easypost_object.js @@ -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; + } } diff --git a/src/services/base_service.js b/src/services/base_service.js index 04938eeed..66aced013 100644 --- a/src/services/base_service.js +++ b/src/services/base_service.js @@ -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'; @@ -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. @@ -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; }); @@ -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