diff --git a/package-lock.json b/package-lock.json
index 3e97b64d9..4d40dcab1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"baseui": "^14.0.0",
"copy-to-clipboard": "^3.3.3",
"lodash": "^4.17.21",
+ "lossless-json": "^4.0.2",
"next": "14.2.16",
"next-logger": "^5.0.0",
"pino": "^9.3.2",
@@ -7836,6 +7837,11 @@
"loose-envify": "cli.js"
}
},
+ "node_modules/lossless-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-4.0.2.tgz",
+ "integrity": "sha512-+z0EaLi2UcWi8MZRxA5iTb6m4Ys4E80uftGY+yG5KNFJb5EceQXOhdW/pWJZ8m97s26u7yZZAYMcKWNztSZssA=="
+ },
"node_modules/lru-cache": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
diff --git a/package.json b/package.json
index d87b08c6e..344296797 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
"baseui": "^14.0.0",
"copy-to-clipboard": "^3.3.3",
"lodash": "^4.17.21",
+ "lossless-json": "^4.0.2",
"next": "14.2.16",
"next-logger": "^5.0.0",
"pino": "^9.3.2",
diff --git a/src/components/pretty-json/pretty-json.types.tsx b/src/components/pretty-json/pretty-json.types.tsx
index 553b2f802..cc76e3d00 100644
--- a/src/components/pretty-json/pretty-json.types.tsx
+++ b/src/components/pretty-json/pretty-json.types.tsx
@@ -1,14 +1,15 @@
export type Props = {
- json: JsonValue;
+ json: PrettyJsonValue;
};
-export type JsonValue =
+export type PrettyJsonValue =
| string
| number
| boolean
| null
| JsonObject
- | JsonArray;
+ | JsonArray
+ | bigint;
-type JsonObject = { [key: string]: JsonValue };
-type JsonArray = JsonValue[];
+type JsonObject = { [key: string]: PrettyJsonValue };
+type JsonArray = PrettyJsonValue[];
diff --git a/src/route-handlers/query-workflow/query-workflow.ts b/src/route-handlers/query-workflow/query-workflow.ts
index 67a08bb2f..0bc1b251a 100644
--- a/src/route-handlers/query-workflow/query-workflow.ts
+++ b/src/route-handlers/query-workflow/query-workflow.ts
@@ -46,6 +46,8 @@ export async function queryWorkflow(
},
});
+ // TODO @assem.hafez: we may loss numeric percision here as we are parsing a result from other languages that may include long numbers
+ // one options is not to parse the result and return it as string and handle the parsing on the client side
return NextResponse.json({
result: res.queryResult
? JSON.parse(
diff --git a/src/utils/__tests__/lossless-json-parse.test.ts b/src/utils/__tests__/lossless-json-parse.test.ts
new file mode 100644
index 000000000..0bfe1ce07
--- /dev/null
+++ b/src/utils/__tests__/lossless-json-parse.test.ts
@@ -0,0 +1,52 @@
+import losslessJsonParse from '../lossless-json-parse';
+
+describe('losslessJsonParse', () => {
+ it('should parse large numbers as BigInt', () => {
+ const json = '{"bigNumber": 90071992547409923213}';
+ const result = losslessJsonParse(json);
+ expect(result).toEqual({ bigNumber: BigInt('90071992547409923213') });
+ });
+
+ it('should parse strings of large numbers as string', () => {
+ const json = '{"bigNumber": "90071992547409923213"}';
+ const result = losslessJsonParse(json);
+ expect(result).toEqual({ bigNumber: '90071992547409923213' });
+ });
+
+ it('should parse a JSON string with integers correctly', () => {
+ const json = '{"integer": 42}';
+ const result = losslessJsonParse(json);
+ expect(result).toEqual({ integer: 42 });
+ });
+
+ it('should parse floating point numbers correctly', () => {
+ const json = '{"float": 3.14}';
+ const result = losslessJsonParse(json);
+ expect(result).toEqual({ float: 3.14 });
+ });
+
+ it('should use the provided reviver function', () => {
+ const json = '{"key": "value"}';
+ const reviver = (key: string, value: any) => {
+ if (key === 'key') {
+ return 'newValue';
+ }
+ return value;
+ };
+ const result = losslessJsonParse(json, reviver);
+ expect(result).toEqual({ key: 'newValue' });
+ });
+
+ it('should handle null and undefined reviver correctly', () => {
+ const json = '{"key": "value"}';
+ const resultWithNullReviver = losslessJsonParse(json, null);
+ const resultWithUndefinedReviver = losslessJsonParse(json, undefined);
+ expect(resultWithNullReviver).toEqual({ key: 'value' });
+ expect(resultWithUndefinedReviver).toEqual({ key: 'value' });
+ });
+
+ it('should throw an error if the JSON is invalid', () => {
+ const json = '{"key": "value"';
+ expect(() => losslessJsonParse(json)).toThrow();
+ });
+});
diff --git a/src/utils/__tests__/lossless-json-stringify.test.ts b/src/utils/__tests__/lossless-json-stringify.test.ts
new file mode 100644
index 000000000..914cad3b9
--- /dev/null
+++ b/src/utils/__tests__/lossless-json-stringify.test.ts
@@ -0,0 +1,65 @@
+import losslessJsonStringify from '../lossless-json-stringify';
+
+describe('losslessJsonStringify', () => {
+ it('should stringify a JSON object with safe numbers correctly', () => {
+ const json = { number: 123 };
+ const result = losslessJsonStringify(json);
+ expect(result).toBe('{"number":123}');
+ });
+
+ it('should stringify BigInts as numbers', () => {
+ const json = { bigNumber: BigInt('900719925474099223423423') };
+ const result = losslessJsonStringify(json);
+ expect(result).toBe('{"bigNumber":900719925474099223423423}');
+ });
+
+ it('should stringify integers correctly', () => {
+ const json = { integer: 42 };
+ const result = losslessJsonStringify(json);
+ expect(result).toBe('{"integer":42}');
+ });
+
+ it('should stringify floating point numbers correctly', () => {
+ const json = { float: 3.14 };
+ const result = losslessJsonStringify(json);
+ expect(result).toBe('{"float":3.14}');
+ });
+
+ it('should use the provided reviver function', () => {
+ const json = { key: 'value' };
+ const reviver = (key: string, value: any) => {
+ if (key === 'key') {
+ return 'newValue';
+ }
+ return value;
+ };
+ const result = losslessJsonStringify(json, reviver);
+ expect(result).toBe('{"key":"newValue"}');
+ });
+
+ it('should handle null and undefined reviver correctly', () => {
+ const json = { key: 'value' };
+ const resultWithNullReviver = losslessJsonStringify(json, null);
+ const resultWithUndefinedReviver = losslessJsonStringify(json, undefined);
+ expect(resultWithNullReviver).toBe('{"key":"value"}');
+ expect(resultWithUndefinedReviver).toBe('{"key":"value"}');
+ });
+
+ it('should format JSON with spaces correctly', () => {
+ const json = { key: 'value' };
+ const result = losslessJsonStringify(json, null, 2);
+ expect(result).toBe('{\n "key": "value"\n}');
+ });
+
+ it('should return an empty string for invalid JSON', () => {
+ const json = () => {};
+ const result = losslessJsonStringify(json);
+ expect(result).toBe('');
+ });
+
+ it('should remove invalid JSON keys', () => {
+ const json = { key: 'value', fn: () => {} };
+ const result = losslessJsonStringify(json);
+ expect(result).toBe('{"key":"value"}');
+ });
+});
diff --git a/src/utils/data-formatters/__tests__/format-payload.test.ts b/src/utils/data-formatters/__tests__/format-payload.test.ts
index 61243dabe..79fb113c0 100644
--- a/src/utils/data-formatters/__tests__/format-payload.test.ts
+++ b/src/utils/data-formatters/__tests__/format-payload.test.ts
@@ -12,8 +12,11 @@ describe('formatPayload', () => {
});
it('should parse JSON data correctly', () => {
- const payload = { data: btoa(JSON.stringify({ key: 'value' })) };
- expect(formatPayload(payload)).toEqual({ key: 'value' });
+ const payload = { data: btoa('{"key":"value","long":284789263475236586}') };
+ expect(formatPayload(payload)).toEqual({
+ key: 'value',
+ long: BigInt('284789263475236586'),
+ });
});
it('should remove double quotes from the string if JSON parsing fails', () => {
diff --git a/src/utils/data-formatters/format-input-payload.ts b/src/utils/data-formatters/format-input-payload.ts
index ea5ce1759..6f09a9eca 100644
--- a/src/utils/data-formatters/format-input-payload.ts
+++ b/src/utils/data-formatters/format-input-payload.ts
@@ -1,5 +1,6 @@
import logger from '@/utils/logger';
+import losslessJsonParse from '../lossless-json-parse';
const formatInputPayload = (
payload: { data?: string | null } | null | undefined
) => {
@@ -25,7 +26,7 @@ function parseJsonLines(input: string) {
try {
// Try to parse the current JSON string
- const jsonObject = JSON.parse(currentJson);
+ const jsonObject = losslessJsonParse(currentJson);
// If successful, add the object to the array
jsonArray.push(jsonObject);
// Reset currentJson for the next JSON object
@@ -38,7 +39,7 @@ function parseJsonLines(input: string) {
// Handle case where the last JSON object might be malformed
if (currentJson.trim() !== '') {
try {
- const jsonObject = JSON.parse(currentJson);
+ const jsonObject = losslessJsonParse(currentJson);
jsonArray.push(jsonObject);
} catch {
logger.error(
diff --git a/src/utils/data-formatters/format-payload.ts b/src/utils/data-formatters/format-payload.ts
index 64230e9df..7fd01af32 100644
--- a/src/utils/data-formatters/format-payload.ts
+++ b/src/utils/data-formatters/format-payload.ts
@@ -1,3 +1,5 @@
+import losslessJsonParse from '../lossless-json-parse';
+
const formatPayload = (
payload: { data?: string | null } | null | undefined
) => {
@@ -11,7 +13,7 @@ const formatPayload = (
// try parsing as JSON
try {
- return JSON.parse(parsedData);
+ return losslessJsonParse(parsedData);
} catch {
// remove double quotes from the string
const formattedString = parsedData.replace(/"/g, '');
diff --git a/src/utils/data-formatters/format-workflow-history-event/__tests__/index.test.ts.snapshot b/src/utils/data-formatters/format-workflow-history-event/__tests__/index.test.ts.snapshot
index f1f8ae104..9c9ddeb72 100644
--- a/src/utils/data-formatters/format-workflow-history-event/__tests__/index.test.ts.snapshot
+++ b/src/utils/data-formatters/format-workflow-history-event/__tests__/index.test.ts.snapshot
@@ -73,7 +73,7 @@ exports[`formatWorkflowHistoryEvent should format workflow activityTaskScheduled
},
"heartbeatTimeoutSeconds": 0,
"input": [
- 1725747370575410000,
+ 1725747370575409843n,
"gadence-canary-xdc",
"workflow.sanity",
],
@@ -523,7 +523,7 @@ exports[`formatWorkflowHistoryEvent should format workflow startChildWorkflowExe
"fields": {},
},
"input": [
- 1726492751798812400,
+ 1726492751798812308n,
30000000000,
],
"jitterStart": null,
diff --git a/src/utils/lossless-json-parse.ts b/src/utils/lossless-json-parse.ts
new file mode 100644
index 000000000..fa6ef56b2
--- /dev/null
+++ b/src/utils/lossless-json-parse.ts
@@ -0,0 +1,16 @@
+import { parse, isSafeNumber, isInteger, type Reviver } from 'lossless-json';
+
+export default function losslessJsonParse(
+ json: string,
+ reviver?: Reviver | null | undefined
+) {
+ return parse(json, reviver, (value) => {
+ if (!isSafeNumber(value)) {
+ return BigInt(value);
+ }
+ if (isInteger(value)) {
+ return parseInt(value);
+ }
+ return parseFloat(value);
+ });
+}
diff --git a/src/utils/lossless-json-stringify.ts b/src/utils/lossless-json-stringify.ts
new file mode 100644
index 000000000..ea7092d05
--- /dev/null
+++ b/src/utils/lossless-json-stringify.ts
@@ -0,0 +1,16 @@
+import { stringify, type Reviver } from 'lossless-json';
+
+export default function losslessJsonStringify(
+ json: unknown,
+ reviver?: Reviver | null | undefined,
+ space?: string | number | undefined
+) {
+ const stringified = stringify(json, reviver, space, [
+ {
+ test: (value) => typeof value === 'bigint',
+ stringify: (value) => (value || '').toString(),
+ },
+ ]);
+
+ return stringified || '';
+}
diff --git a/src/views/workflow-history/workflow-history-event-details-json/__tests__/workflow-summary-tab-json-view.test.tsx b/src/views/workflow-history/workflow-history-event-details-json/__tests__/workflow-summary-tab-json-view.test.tsx
index 01dd1c539..347ad07b3 100644
--- a/src/views/workflow-history/workflow-history-event-details-json/__tests__/workflow-summary-tab-json-view.test.tsx
+++ b/src/views/workflow-history/workflow-history-event-details-json/__tests__/workflow-summary-tab-json-view.test.tsx
@@ -2,6 +2,8 @@ import React from 'react';
import { render } from '@/test-utils/rtl';
+import losslessJsonStringify from '@/utils/lossless-json-stringify';
+
import WorkflowSummaryTabJsonView from '../workflow-history-event-details-json';
jest.mock('@/components/copy-text-button/copy-text-button', () =>
@@ -13,11 +15,14 @@ jest.mock('@/components/pretty-json/pretty-json', () =>
);
describe('WorkflowSummaryTabJsonView Component', () => {
- const inputJson = { input: 'inputJson' };
+ const losslessInputJson = {
+ input: 'inputJson',
+ long: BigInt('9007199254740991345435'),
+ };
it('renders correctly with initial props', () => {
const { getByText } = render(
-