Skip to content

Commit c0ecd95

Browse files
bayulaksana479andrewklau
authored andcommitted
fix: safe stringify
1 parent 0bd5944 commit c0ecd95

File tree

1 file changed

+98
-0
lines changed

1 file changed

+98
-0
lines changed

apps/frontend/src/lib/json.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
type JsonPrimitive = boolean | null | number | string;
2+
type JsonValue = JsonArray | JsonObject | JsonPrimitive;
3+
interface JsonObject {
4+
[key: string]: JsonValue;
5+
}
6+
type JsonArray = JsonValue[];
7+
8+
interface SafeStringifyOptions {
9+
indentation?: number | string;
10+
trace?: boolean;
11+
}
12+
13+
interface Serializable {
14+
toJSON(): unknown;
15+
}
16+
17+
const hasToJSON = (value: unknown): value is Serializable => {
18+
return (
19+
value !== null &&
20+
value !== undefined &&
21+
typeof (value as Serializable).toJSON === 'function'
22+
);
23+
};
24+
25+
const serializeValue = (
26+
value: unknown,
27+
seen: WeakMap<object, string>,
28+
trace: boolean,
29+
currentPath: string,
30+
): JsonValue => {
31+
if (hasToJSON(value)) {
32+
value = value.toJSON();
33+
}
34+
35+
if (typeof value === 'bigint') {
36+
if (value >= Number.MIN_SAFE_INTEGER && value <= Number.MAX_SAFE_INTEGER) {
37+
return Number(value);
38+
}
39+
return value.toString();
40+
}
41+
42+
if (value === null || value === undefined) {
43+
return null;
44+
}
45+
46+
if (typeof value !== 'object') {
47+
return value as JsonPrimitive;
48+
}
49+
50+
const objectValue = value as Record<string, unknown>;
51+
52+
if (seen.has(objectValue)) {
53+
if (!trace) {
54+
return '[Circular]';
55+
}
56+
57+
const existingPath = seen.get(objectValue)!;
58+
const circularPath = existingPath === '' ? '*' : `*${existingPath}`;
59+
return `[Circular ${circularPath}]`;
60+
}
61+
62+
seen.set(objectValue, currentPath);
63+
64+
let newValue: JsonArray | JsonObject;
65+
66+
if (Array.isArray(objectValue)) {
67+
newValue = objectValue.map((item: unknown, index: number) => {
68+
const nextPath =
69+
currentPath === '' ? `${index}` : `${currentPath}.${index}`;
70+
return serializeValue(item, seen, trace, nextPath);
71+
});
72+
} else {
73+
newValue = {} as JsonObject;
74+
for (const [propertyKey, propertyValue] of Object.entries(objectValue)) {
75+
const nextPath =
76+
currentPath === '' ? propertyKey : `${currentPath}.${propertyKey}`;
77+
(newValue as JsonObject)[propertyKey] = serializeValue(
78+
propertyValue,
79+
seen,
80+
trace,
81+
nextPath,
82+
);
83+
}
84+
}
85+
86+
seen.delete(objectValue);
87+
88+
return newValue;
89+
};
90+
91+
export const safeStringify = (
92+
value: unknown,
93+
{ indentation, trace = false }: SafeStringifyOptions = {},
94+
): string => {
95+
const seen = new WeakMap<object, string>();
96+
const serializedValue = serializeValue(value, seen, trace, '');
97+
return JSON.stringify(serializedValue, undefined, indentation);
98+
};

0 commit comments

Comments
 (0)