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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
### v4.11.0 (2026-06-29)
* * *
### Bug Fixes:
- Fixed JSON request body serialization where `Timestamp` parameters were sent in a human-readable format (e.g. `"expires_at":"2026-06-23 09:54:44.513"`) instead of Unix seconds. They are now serialized as numeric Unix seconds (e.g. `"expires_at":1782189229`), matching the form-url-encoded path and the format expected by the API. This affects all JSON content-type endpoints, such as [`create_promotional_grant`](https://apidocs.chargebee.com/docs/api/promotional_grants/create-promotional-grant) in [`PromotionalGrant`](https://apidocs.chargebee.com/docs/api/promotional_grants).
- Fixed JSON request body serialization for endpoints that accept nested objects and arrays. Previously the JSON body reused the `application/x-www-form-urlencoded` flattening (e.g. `events[deduplication_id][]`) and double-encoded nested objects; the JSON content-type now emits a proper nested structure. This affects JSON endpoints such as [`ingest_usages_in_batch`](https://apidocs.chargebee.com/docs/api/usage_events/ingest-usages-in-batch) and [`create_a_usage_event`](https://apidocs.chargebee.com/docs/api/usage_events/create-a-usage-event) in [`UsageEvent`](https://apidocs.chargebee.com/docs/api/usage_events).
- Fixed `usage_timestamp` for [`UsageEvent`](https://apidocs.chargebee.com/docs/api/usage_events) to be sent as epoch milliseconds. The `usage_timestamp` parameter in [`create_a_usage_event`](https://apidocs.chargebee.com/docs/api/usage_events/create-a-usage-event) and [`ingest_usages_in_batch`](https://apidocs.chargebee.com/docs/api/usage_events/ingest-usages-in-batch) is now typed as `Long` (epoch milliseconds) instead of `Timestamp`, preserving the millisecond precision required by the ingestion API.



### v4.10.0 (2026-06-12)
* * *
### New Resources:
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.10.0
4.11.0
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ plugins {
}

group = "com.chargebee"
version = "4.10.0"
version = "4.11.0"
description = "Java client library for ChargeBee"

// Project metadata
Expand Down
69 changes: 32 additions & 37 deletions src/main/java/com/chargebee/v4/internal/JsonUtil.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
package com.chargebee.v4.internal;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializer;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -19,6 +23,29 @@
/** Gson-backed JSON parsing utility. */
public class JsonUtil {

/**
* Gson instance configured with Chargebee-specific serializers so that
* non-trivial leaf types are emitted in the shape the Chargebee API expects:
* <ul>
* <li>{@link Timestamp} → Unix seconds (JSON number)</li>
* <li>{@link Date} → {@code "yyyy-MM-dd"} JSON string</li>
* <li>{@link Enum} → lowercase {@code name()} JSON string</li>
* </ul>
* HTML escaping is disabled so characters like {@code <}, {@code >}, {@code &}
* pass through verbatim (matches the previous {@code JsonElement.toString()} behavior).
*/
private static final Gson GSON = new GsonBuilder()
.disableHtmlEscaping()
.serializeNulls()
.registerTypeAdapter(Timestamp.class,
(JsonSerializer<Timestamp>) (src, t, c) -> new JsonPrimitive(src.getTime()))
Comment thread
cb-alish marked this conversation as resolved.
.registerTypeAdapter(Date.class,
(JsonSerializer<Date>) (src, t, c) ->
new JsonPrimitive(new SimpleDateFormat("yyyy-MM-dd").format(src)))
.registerTypeHierarchyAdapter(Enum.class,
(JsonSerializer<Enum<?>>) (src, t, c) -> new JsonPrimitive(src.name().toLowerCase()))
Comment thread
cb-alish marked this conversation as resolved.
.create();

private JsonUtil() {}

// --- Parse entry points ---
Expand Down Expand Up @@ -320,25 +347,16 @@ public static Map<String, Object> extractConsentFields(JsonObject obj, Set<Strin

// --- Serialization ---

/** Serializes a Map to a JSON string. */
@SuppressWarnings("unchecked")
/** Serializes a Map to a JSON string using the configured {@link #GSON} instance. */
public static String toJson(Map<String, Object> map) {
if (map == null || map.isEmpty()) return "{}";
JsonObject obj = new JsonObject();
for (Map.Entry<String, Object> entry : map.entrySet()) {
obj.add(entry.getKey(), toJsonElement(entry.getValue()));
}
return obj.toString();
return GSON.toJson(map);
}

/** Serializes a List to a JSON string. */
/** Serializes a List to a JSON string using the configured {@link #GSON} instance. */
public static String toJson(List<?> list) {
if (list == null || list.isEmpty()) return "[]";
JsonArray array = new JsonArray();
for (Object item : list) {
array.add(toJsonElement(item));
}
return array.toString();
return GSON.toJson(list);
}

// --- Internal helpers ---
Expand All @@ -365,27 +383,4 @@ private static Object toJavaValue(JsonElement value) {
}
return null;
}

@SuppressWarnings("unchecked")
private static JsonElement toJsonElement(Object value) {
if (value == null) return JsonNull.INSTANCE;
if (value instanceof String) return new JsonPrimitive((String) value);
if (value instanceof Number) return new JsonPrimitive((Number) value);
if (value instanceof Boolean) return new JsonPrimitive((Boolean) value);
if (value instanceof Map) {
JsonObject obj = new JsonObject();
for (Map.Entry<String, Object> entry : ((Map<String, Object>) value).entrySet()) {
obj.add(entry.getKey(), toJsonElement(entry.getValue()));
}
return obj;
}
if (value instanceof List) {
JsonArray array = new JsonArray();
for (Object item : (List<?>) value) {
array.add(toJsonElement(item));
}
return array;
}
return new JsonPrimitive(value.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public static final class EntityIdFilter extends StringFilter<EntitlementListBui
}
}

public enum EntityTypeIs {
public enum EntityTypeIn {
PLAN("plan"),

ADDON("addon"),
Expand All @@ -168,21 +168,21 @@ public enum EntityTypeIs {

ADDON_PRICE("addon_price"),

/** An enum member indicating that EntityTypeIs was instantiated with an unknown value. */
/** An enum member indicating that EntityTypeIn was instantiated with an unknown value. */
_UNKNOWN(null);
private final String value;

EntityTypeIs(String value) {
EntityTypeIn(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static EntityTypeIs fromString(String value) {
public static EntityTypeIn fromString(String value) {
if (value == null) return _UNKNOWN;
for (EntityTypeIs enumValue : EntityTypeIs.values()) {
for (EntityTypeIn enumValue : EntityTypeIn.values()) {
if (enumValue.value != null && enumValue.value.equals(value)) {
return enumValue;
}
Expand All @@ -191,7 +191,7 @@ public static EntityTypeIs fromString(String value) {
}
}

public enum EntityTypeIn {
public enum EntityTypeIs {
PLAN("plan"),

ADDON("addon"),
Expand All @@ -202,21 +202,21 @@ public enum EntityTypeIn {

ADDON_PRICE("addon_price"),

/** An enum member indicating that EntityTypeIn was instantiated with an unknown value. */
/** An enum member indicating that EntityTypeIs was instantiated with an unknown value. */
_UNKNOWN(null);
private final String value;

EntityTypeIn(String value) {
EntityTypeIs(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static EntityTypeIn fromString(String value) {
public static EntityTypeIs fromString(String value) {
if (value == null) return _UNKNOWN;
for (EntityTypeIn enumValue : EntityTypeIn.values()) {
for (EntityTypeIs enumValue : EntityTypeIs.values()) {
if (enumValue.value != null && enumValue.value.equals(value)) {
return enumValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,59 @@ public Map<String, Object> toFormData() {
return formData;
}

/**
* Get the nested JSON body representation for this request.
*
* <p>Unlike {@link #toFormData()}, which flattens nested objects/arrays into bracketed {@code
* qs}-style keys for {@code application/x-www-form-urlencoded} requests, this method preserves
* the real object/array hierarchy so the payload can be serialized as a JSON string. Leaf values
* (e.g. {@code Timestamp}) are converted to their API representation by {@link JsonUtil} during
* serialization.
*/
public Map<String, Object> toJsonMap() {
Map<String, Object> jsonData = new LinkedHashMap<>();

if (this.id != null) {

jsonData.put("id", this.id);
}

if (this.subscriptionId != null) {

jsonData.put("subscription_id", this.subscriptionId);
}

if (this.unitId != null) {

jsonData.put("unit_id", this.unitId);
}

if (this.amount != null) {

jsonData.put("amount", this.amount);
}

if (this.ledgerOperationTimestamp != null) {

jsonData.put("ledger_operation_timestamp", this.ledgerOperationTimestamp);
}

if (this.autoReleaseTimestamp != null) {

jsonData.put("auto_release_timestamp", this.autoReleaseTimestamp);
}

if (this.metadata != null) {

jsonData.put("metadata", this.metadata);
}

return jsonData;
}

/** Get the JSON string representation for this request. */
public String toJsonString() {
return JsonUtil.toJson(toFormData());
return JsonUtil.toJson(toJsonMap());
}

/** Create a new builder for LedgerOperationAuthorizeParams. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,49 @@ public Map<String, Object> toFormData() {
return formData;
}

/**
* Get the nested JSON body representation for this request.
*
* <p>Unlike {@link #toFormData()}, which flattens nested objects/arrays into bracketed {@code
* qs}-style keys for {@code application/x-www-form-urlencoded} requests, this method preserves
* the real object/array hierarchy so the payload can be serialized as a JSON string. Leaf values
* (e.g. {@code Timestamp}) are converted to their API representation by {@link JsonUtil} during
* serialization.
*/
public Map<String, Object> toJsonMap() {
Map<String, Object> jsonData = new LinkedHashMap<>();

if (this.authorizationId != null) {

jsonData.put("authorization_id", this.authorizationId);
}

if (this.id != null) {

jsonData.put("id", this.id);
}

if (this.amount != null) {

jsonData.put("amount", this.amount);
}

if (this.ledgerOperationTimestamp != null) {

jsonData.put("ledger_operation_timestamp", this.ledgerOperationTimestamp);
}

if (this.metadata != null) {

jsonData.put("metadata", this.metadata);
}

return jsonData;
}

/** Get the JSON string representation for this request. */
public String toJsonString() {
return JsonUtil.toJson(toFormData());
return JsonUtil.toJson(toJsonMap());
}

/** Create a new builder for LedgerOperationCaptureAuthorizationParams. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,54 @@ public Map<String, Object> toFormData() {
return formData;
}

/**
* Get the nested JSON body representation for this request.
*
* <p>Unlike {@link #toFormData()}, which flattens nested objects/arrays into bracketed {@code
* qs}-style keys for {@code application/x-www-form-urlencoded} requests, this method preserves
* the real object/array hierarchy so the payload can be serialized as a JSON string. Leaf values
* (e.g. {@code Timestamp}) are converted to their API representation by {@link JsonUtil} during
* serialization.
*/
public Map<String, Object> toJsonMap() {
Map<String, Object> jsonData = new LinkedHashMap<>();

if (this.id != null) {

jsonData.put("id", this.id);
}

if (this.subscriptionId != null) {

jsonData.put("subscription_id", this.subscriptionId);
}

if (this.unitId != null) {

jsonData.put("unit_id", this.unitId);
}

if (this.amount != null) {

jsonData.put("amount", this.amount);
}

if (this.ledgerOperationTimestamp != null) {

jsonData.put("ledger_operation_timestamp", this.ledgerOperationTimestamp);
}

if (this.metadata != null) {

jsonData.put("metadata", this.metadata);
}

return jsonData;
}

/** Get the JSON string representation for this request. */
public String toJsonString() {
return JsonUtil.toJson(toFormData());
return JsonUtil.toJson(toJsonMap());
}

/** Create a new builder for LedgerOperationCaptureParams. */
Expand Down
Loading
Loading