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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
### v3.27.0 (2026-06-30)
* * *
### Enhancements:
- **Zod-backed request validation** — Set `enableValidation: true` in the client configuration to validate outgoing request parameters against each endpoint's generated Zod schema before the API call is sent. Invalid payloads raise `ChargebeeZodValidationError`, which carries the offending `actionName` and the original `ZodError` (including `issues` and `flatten()`) for inspection.
- **Runtime dependency** — Added [`zod`](https://www.npmjs.com/package/zod) (v4) as a runtime dependency to support request validation.


### v3.26.0 (2026-06-12)
* * *
### New Resources:
Expand Down
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,45 @@ try {
}
```

### Request parameter validation (Zod)

When `enableValidation` is set to `true`, the SDK validates parameters for **every** API request against Zod schemas **before** the HTTP call is made. If you omit the params object on a call, it is validated as `{}`. This is **off by default**. Schemas are included for API actions that support them; actions without a bundled schema behave as usual.

```typescript
import Chargebee, { ChargebeeZodValidationError } from 'chargebee';

const chargebee = new Chargebee({
site: '{{site}}',
apiKey: '{{api-key}}',
enableValidation: true,
});

try {
await chargebee.customer.create({
id: 'a'.repeat(100),
auto_collection: 'invalid',
});
} catch (err) {
if (err instanceof ChargebeeZodValidationError) {
console.error(err.message);
console.error(err.actionName);
console.error(err.zodError.issues);
} else {
throw err;
}
}
```

Invalid parameters produce a `ChargebeeZodValidationError`. The error message lists every problem (field path and message). You can also inspect `actionName` (the API action, for example `create`) and `zodError` (Zod’s `ZodError`, including `issues`) for structured handling.

**Example message:**

```text
ChargebeeZodValidationError: [Chargebee] Validation failed for 'create': id: Too big: expected string to have <=50 characters; auto_collection: Invalid option: expected one of "on"|"off"
```

The same `ChargebeeZodValidationError` shape applies to any action with a schema when parameters are invalid (for example bad filters or limits on `list`).

### Using filters in the List API

For pagination, `offset` is the parameter that is being used. The value used for this parameter must be the value returned for `next_offset` parameter in the previous API call.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.26.0
3.27.0
16 changes: 14 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "chargebee",
"version": "3.26.0",
"version": "3.27.0",
"description": "A library for integrating with Chargebee.",
"scripts": {
"prepack": "npm install && npm run build",
Expand Down Expand Up @@ -75,5 +75,8 @@
"semi": true,
"singleQuote": true,
"parser": "typescript"
},
"dependencies": {
"zod": "^4.3.6"
}
}
46 changes: 46 additions & 0 deletions src/RequestWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import {
} from './types.js';
import { handleResponse } from './coreCommon.js';
import { Buffer } from 'node:buffer';
import type { ZodObject, ZodRawShape } from 'zod';
import { ChargebeeZodValidationError } from './chargebeeZodValidationError.js';
import { getSchema } from './validationLoader.js';

export class RequestWrapper {
private readonly args: IArguments;
Expand All @@ -42,6 +45,22 @@ export class RequestWrapper {
return idParam;
}

/**
* Validate parameters against the action's Zod schema when enableValidation is true.
* Query params are validated as `params ?? {}`; body params are validated when `params` is non-null.
* Throws a descriptive error listing every validation violation.
*/
private static _validateParams(
params: JSONValue,
schema: ZodObject<ZodRawShape>,
actionName: string,
): void {
const result = schema.safeParse(params);
if (!result.success) {
throw new ChargebeeZodValidationError(actionName, result.error);
}
}

private static parseRetryAfter(retryAfter?: string): number | null {
if (!retryAfter) return null;
const seconds = parseInt(retryAfter, 10);
Expand Down Expand Up @@ -71,6 +90,33 @@ export class RequestWrapper {
: this.args[0];
let headers = this.apiCall.hasIdInUrl ? this.args[2] : this.args[1];

// Lazy-load Zod schema when enableValidation is true
if (
env.enableValidation &&
this.apiCall.resourceKey &&
this.apiCall.actionName
) {
const schema = await getSchema(
this.apiCall.resourceKey,
this.apiCall.actionName,
);
if (schema) {
if (this.apiCall.httpMethod === 'GET') {
RequestWrapper._validateParams(
params ?? {},
schema,
this.apiCall.methodName,
);
} else if (params != null) {
RequestWrapper._validateParams(
params,
schema,
this.apiCall.methodName,
);
}
}
Comment on lines +104 to +117

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Omitted non-GET params skip validation, contradicting the docs.

For non-GET, validation only runs when params != null. An argument-less call (e.g. cb.invoice.charge()) leaves params undefined → validation is skipped, the {} body is sent, and required-field schemas never fire. README (Line 129) promises omitted params are validated as {} for every request. Either align the code (preferred, catches missing required fields) or fix the doc.

The existing test only exercises cb.invoice.charge({}) (truthy {}), so it misses this path.

🔧 Align non-GET with documented behavior
-        if (this.apiCall.httpMethod === 'GET') {
-          RequestWrapper._validateParams(
-            params ?? {},
-            schema,
-            this.apiCall.methodName,
-          );
-        } else if (params != null) {
-          RequestWrapper._validateParams(
-            params,
-            schema,
-            this.apiCall.methodName,
-          );
-        }
+        RequestWrapper._validateParams(
+          params ?? {},
+          schema,
+          this.apiCall.methodName,
+        );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this.apiCall.httpMethod === 'GET') {
RequestWrapper._validateParams(
params ?? {},
schema,
this.apiCall.methodName,
);
} else if (params != null) {
RequestWrapper._validateParams(
params,
schema,
this.apiCall.methodName,
);
}
}
RequestWrapper._validateParams(
params ?? {},
schema,
this.apiCall.methodName,
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/RequestWrapper.ts` around lines 104 - 117, The non-GET branch in
RequestWrapper currently skips validation when params is undefined, so calls
like cb.invoice.charge() bypass required-field checks and diverge from the
documented behavior. Update RequestWrapper._validateParams usage in
RequestWrapper so non-GET requests validate params ?? {} the same way GET does,
ensuring omitted arguments are treated as an empty object. Also extend the
existing tests around the charge/invoice path to cover the argument-less call
case, not just cb.invoice.charge({}).

}

Object.assign(this.httpHeaders, headers);

if (
Expand Down
4 changes: 4 additions & 0 deletions src/chargebee.cjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
WebhookPayloadParseError,
} from './resources/webhook/handler.js';
import { basicAuthValidator } from './resources/webhook/auth.js';
import { ChargebeeZodValidationError } from './chargebeeZodValidationError.js';

const httpClient = new FetchHttpClient();
const Chargebee = CreateChargebee(httpClient);
Expand All @@ -27,6 +28,9 @@ module.exports.WebhookAuthenticationError = WebhookAuthenticationError;
module.exports.WebhookPayloadValidationError = WebhookPayloadValidationError;
module.exports.WebhookPayloadParseError = WebhookPayloadParseError;

// Export validation error class
module.exports.ChargebeeZodValidationError = ChargebeeZodValidationError;

// Export webhook types
export type {
WebhookEvent,
Expand Down
2 changes: 2 additions & 0 deletions src/chargebee.cjs.worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CreateChargebee } from './createChargebee.js';
import { FetchHttpClient } from './net/FetchClient.js';
import { ChargebeeZodValidationError } from './chargebeeZodValidationError.js';

const httpClient = new FetchHttpClient();
const Chargebee = CreateChargebee(httpClient);
Expand Down Expand Up @@ -29,3 +30,4 @@ export type {
RequestValidator,
} from './resources/webhook/handler.js';
export type { CredentialValidator } from './resources/webhook/auth.js';
module.exports.ChargebeeZodValidationError = ChargebeeZodValidationError;
3 changes: 3 additions & 0 deletions src/chargebee.esm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export {
WebhookPayloadParseError,
} from './resources/webhook/handler.js';

// Export validation error class
export { ChargebeeZodValidationError } from './chargebeeZodValidationError.js';

// Export webhook types
export type {
WebhookEvent,
Expand Down
1 change: 1 addition & 0 deletions src/chargebee.esm.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ export type {
RequestValidator,
} from './resources/webhook/handler.js';
export type { CredentialValidator } from './resources/webhook/auth.js';
export { ChargebeeZodValidationError } from './chargebeeZodValidationError.js';
18 changes: 18 additions & 0 deletions src/chargebeeZodValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { ZodError } from 'zod';

export class ChargebeeZodValidationError extends Error {
readonly actionName: string;
readonly zodError: ZodError;

constructor(actionName: string, zodError: ZodError) {
const messages = zodError.issues
.map((e) => `${e.path.join('.')}: ${e.message}`)
.join('; ');
super(`[Chargebee] Validation failed for '${actionName}': ${messages}`);
Object.setPrototypeOf(this, new.target.prototype);
this.name = 'ChargebeeZodValidationError';
this.actionName = actionName;
this.zodError = zodError;
Error.captureStackTrace?.(this, this.constructor);
}
}
5 changes: 5 additions & 0 deletions src/createChargebee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export const CreateChargebee = (httpClient: HttpClientInterface) => {
jsonKeys: metaArr[7],
options: metaArr[8],
};
if (this._env.enableValidation) {
// Store resource and action for lazy schema loading in RequestWrapper
apiCall.resourceKey = res;
apiCall.actionName = metaArr[0] as string;
}
this[res][apiCall.methodName] = this._createApiFunc(
apiCall,
this._env,
Expand Down
2 changes: 1 addition & 1 deletion src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const Environment = {
hostSuffix: '.chargebee.com',
apiPath: '/api/v2',
timeout: DEFAULT_TIME_OUT,
clientVersion: 'v3.26.0',
clientVersion: 'v3.27.0',
port: DEFAULT_PORT,
timemachineWaitInMillis: DEFAULT_TIME_MACHINE_WAIT,
exportWaitInMillis: DEFAULT_EXPORT_WAIT,
Expand Down
Loading
Loading