diff --git a/docs/ja/reference/util/safeJSONParse.md b/docs/ja/reference/util/safeJSONParse.md new file mode 100644 index 000000000..794f4a88a --- /dev/null +++ b/docs/ja/reference/util/safeJSONParse.md @@ -0,0 +1,177 @@ +# safeJSONParse + +エラーをスローせずに JSON 文字列を安全にパースします。 + +```typescript +const result = safeJSONParse(value); +``` + +## 使用法 + +### `safeJSONParse(value)` + +JSON 文字列を安全にパースしたい場合に `safeJSONParse` を使用してください。`JSON.parse` 例外を発生させるリスクなくパースできます。値が有効な JSON を含む文字列の場合、パースされた結果を返します。そうでない場合、`safeJSONParse` は `null` を返します。 + +ユーザー入力、緩く型付けされたデータ、または値が常に有効な JSON 文字列であるという保証がない信頼できないソースを扱う際に特に便利です。 + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +// 有効な JSON 文字列 +safeJSONParse('{"name":"JinHo","age":29}'); // { name: "JinHo", age: 29 } +safeJSONParse('[1, 2, 3]'); // [1, 2, 3] +safeJSONParse('"hello world"'); // "hello world" +safeJSONParse('42'); // 42 +safeJSONParse('true'); // true +safeJSONParse('false'); // false +safeJSONParse('null'); // null (有効な JSON ですが、パースされた値が null) + +// 無効な JSON 文字列 +safeJSONParse('invalid json'); // null +safeJSONParse('{"unclosed": "object"'); // null +safeJSONParse('[1,2,'); // null +safeJSONParse('undefined'); // null +safeJSONParse(''); // null + +// 文字列でない値 +safeJSONParse({ name: 'JinHo' }); // null +safeJSONParse([1, 2, 3]); // null +safeJSONParse(42); // null +safeJSONParse(true); // null +safeJSONParse(null); // null +safeJSONParse(undefined); // null +``` + +### ジェネリックを使用した型安全なパース + +ジェネリックパラメータ `T` を使用して、パースされた JSON 値の期待される形状を記述できます: + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +type User = { + id: number; + name: string; + isAdmin: boolean; +}; + +const raw = '{"id":1,"name":"JinHo","isAdmin":false}'; + +const user = safeJSONParse(raw); +// user: User | null + +if (user !== null) { + // user はここで User に型が絞り込まれます + console.log(user.id); // number + console.log(user.name); // string + console.log(user.isAdmin); // boolean +} +``` + +型引数を提供しない場合、戻り値の型はデフォルトで `unknown | null` になります: + +```typescript +const result = safeJSONParse('{"name": "JinHo"}'); +// result: unknown | null +``` + +`safeJSONParse` を独自の型ガード(例: `isJSONObject`, `isJSONValue`)と組み合わせて、パースされた構造をさらに検証できます。 + +### isJSON と一緒に使用する + +`safeJSONParse` は `isJSON` 述語を補完するように設計されています: + +- 値が有効な JSON 文字列かどうかを確認するだけの場合は `isJSON(value)` を使用してください。 +- JSON 文字列を実際にパースして結果を操作したい場合は `safeJSONParse(value)` を使用してください。 + +```typescript +import { isJSON } from 'es-toolkit/predicate'; +import { safeJSONParse } from 'es-toolkit/util'; + +function parseIfJson(input: unknown) { + if (!isJSON(input)) { + // input は有効な JSON 文字列ではありません + return null; + } + + // input は string 型に絞り込まれます + const parsed = safeJSONParse(input); + + // parsed は JSON 値(または予期しないことが発生した場合は null)です + return parsed; +} +``` + +### 実用的な例 + +#### API レスポンスのパース + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +type ApiResponse = { + success: boolean; + payload?: unknown; +}; + +function handleApiResponse(responseBody: unknown) { + const data = safeJSONParse(responseBody); + + if (data === null) { + return { + success: false, + error: 'レスポンス本文が有効な JSON ではありません', + }; + } + + if (!data.success) { + return { + success: false, + error: 'API が失敗を報告しました', + }; + } + + return { + success: true, + payload: data.payload, + }; +} +``` + +#### ユーザー入力のパース + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +function tryParseUserConfig(input: unknown) { + const config = safeJSONParse>(input); + + if (config === null) { + return { + isValid: false, + config: null, + error: '入力が有効な JSON ではありません', + }; + } + + return { + isValid: true, + config, + error: null, + }; +} +``` + +::: info "null" 文字列に関する注意 + +入力が文字列 `"null"` の場合、`safeJSONParse("null")` は実際のパースされた値であるため `null` を返します。これは純粋に戻り値だけではパース失敗と区別できません。2つのケースを区別する必要がある場合は、`isJSON` と `safeJSONParse` を組み合わせるか、カスタム結果型を導入してください。 + +::: + +#### パラメータ + +- `value` (`unknown`): JSON としてパースする値です。 + +#### 戻り値 + +(`T | null`): `value` が有効な JSON を含む文字列の場合、パースされた JSON 値。そうでない場合、`null`。 diff --git a/docs/ko/reference/util/safeJSONParse.md b/docs/ko/reference/util/safeJSONParse.md new file mode 100644 index 000000000..33a211716 --- /dev/null +++ b/docs/ko/reference/util/safeJSONParse.md @@ -0,0 +1,177 @@ +# safeJSONParse + +에러를 던지지 않고 JSON 문자열을 안전하게 파싱해요. + +```typescript +const result = safeJSONParse(value); +``` + +## 사용법 + +### `safeJSONParse(value)` + +JSON 문자열을 안전하게 파싱하고 싶을 때 `safeJSONParse`를 사용하세요. `JSON.parse` 예외를 발생시킬 위험 없이 파싱할 수 있어요. 값이 유효한 JSON을 포함한 문자열이면 파싱된 결과를 반환하고, 그렇지 않으면 `safeJSONParse`는 `null`을 반환해요. + +사용자 입력, 느슨하게 타입이 지정된 데이터, 또는 값이 항상 유효한 JSON 문자열이라는 보장이 없는 신뢰할 수 없는 소스를 다룰 때 특히 유용해요. + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +// 유효한 JSON 문자열들 +safeJSONParse('{"name":"JinHo","age":29}'); // { name: "JinHo", age: 29 } +safeJSONParse('[1, 2, 3]'); // [1, 2, 3] +safeJSONParse('"hello world"'); // "hello world" +safeJSONParse('42'); // 42 +safeJSONParse('true'); // true +safeJSONParse('false'); // false +safeJSONParse('null'); // null (유효한 JSON이지만 파싱된 값이 null) + +// 유효하지 않은 JSON 문자열들 +safeJSONParse('invalid json'); // null +safeJSONParse('{"unclosed": "object"'); // null +safeJSONParse('[1,2,'); // null +safeJSONParse('undefined'); // null +safeJSONParse(''); // null + +// 문자열이 아닌 값들 +safeJSONParse({ name: 'JinHo' }); // null +safeJSONParse([1, 2, 3]); // null +safeJSONParse(42); // null +safeJSONParse(true); // null +safeJSONParse(null); // null +safeJSONParse(undefined); // null +``` + +### 제네릭을 사용한 타입 안전한 파싱 + +제네릭 파라미터 `T`를 사용해서 파싱된 JSON 값의 예상 형태를 설명할 수 있어요: + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +type User = { + id: number; + name: string; + isAdmin: boolean; +}; + +const raw = '{"id":1,"name":"JinHo","isAdmin":false}'; + +const user = safeJSONParse(raw); +// user: User | null + +if (user !== null) { + // user는 여기서 User로 타입이 좁혀져요 + console.log(user.id); // number + console.log(user.name); // string + console.log(user.isAdmin); // boolean +} +``` + +타입 인자를 제공하지 않으면 반환 타입은 기본적으로 `unknown | null`이에요: + +```typescript +const result = safeJSONParse('{"name": "JinHo"}'); +// result: unknown | null +``` + +`safeJSONParse`를 자신만의 타입 가드(예: `isJSONObject`, `isJSONValue`)와 함께 사용해서 파싱된 구조를 더 검증할 수 있어요. + +### isJSON과 함께 사용하기 + +`safeJSONParse`는 `isJSON` 타입 가드를 보완하도록 설계되었어요: + +- 값이 유효한 JSON 문자열인지 확인만 하려면 `isJSON(value)`를 사용하세요. +- JSON 문자열을 실제로 파싱하고 결과를 사용하고 싶을 때는 `safeJSONParse(value)`를 사용하세요. + +```typescript +import { isJSON } from 'es-toolkit/predicate'; +import { safeJSONParse } from 'es-toolkit/util'; + +function parseIfJson(input: unknown) { + if (!isJSON(input)) { + // input은 유효한 JSON 문자열이 아니에요 + return null; + } + + // input은 이제 string 타입으로 좁혀져요 + const parsed = safeJSONParse(input); + + // parsed는 JSON 값(또는 예상치 못한 일이 발생하면 null)이에요 + return parsed; +} +``` + +### 실용적인 예시 + +#### API 응답 파싱 + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +type ApiResponse = { + success: boolean; + payload?: unknown; +}; + +function handleApiResponse(responseBody: unknown) { + const data = safeJSONParse(responseBody); + + if (data === null) { + return { + success: false, + error: '응답 본문이 유효한 JSON이 아니에요', + }; + } + + if (!data.success) { + return { + success: false, + error: 'API가 실패를 보고했어요', + }; + } + + return { + success: true, + payload: data.payload, + }; +} +``` + +#### 사용자 입력 파싱 + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +function tryParseUserConfig(input: unknown) { + const config = safeJSONParse>(input); + + if (config === null) { + return { + isValid: false, + config: null, + error: '입력이 유효한 JSON이 아니에요', + }; + } + + return { + isValid: true, + config, + error: null, + }; +} +``` + +::: info "null" 문자열에 대한 참고 + +입력이 문자열 `"null"`일 때, `safeJSONParse("null")`은 실제 파싱된 값이기 때문에 `null`을 반환해요. 이것은 순수하게 반환 값만으로는 파싱 실패와 구별할 수 없어요. 두 경우를 구별해야 한다면 `isJSON`과 `safeJSONParse`를 함께 사용하거나 커스텀 결과 타입을 도입하세요. + +::: + +#### 파라미터 + +- `value` (`unknown`): JSON으로 파싱할 값이에요. + +#### 반환 값 + +(`T | null`): `value`가 유효한 JSON을 포함한 문자열이면 파싱된 JSON 값, 그렇지 않으면 `null`이에요. diff --git a/docs/reference/util/safeJSONParse.md b/docs/reference/util/safeJSONParse.md new file mode 100644 index 000000000..6827b4de0 --- /dev/null +++ b/docs/reference/util/safeJSONParse.md @@ -0,0 +1,177 @@ +# safeJSONParse + +Safely parses a JSON string without throwing. + +```typescript +const result = safeJSONParse(value); +``` + +## Usage + +### `safeJSONParse(value)` + +Use `safeJSONParse` when you want to parse a JSON string safely, without risking a `JSON.parse` exception. If the value is a string containing valid JSON, the parsed result is returned. Otherwise, `safeJSONParse` returns `null`. + +This is especially useful when dealing with user input, loosely-typed data, or untrusted sources where you can't guarantee that the value is always a valid JSON string. + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +// Valid JSON strings +safeJSONParse('{"name":"JinHo","age":29}'); // { name: "JinHo", age: 29 } +safeJSONParse('[1, 2, 3]'); // [1, 2, 3] +safeJSONParse('"hello world"'); // "hello world" +safeJSONParse('42'); // 42 +safeJSONParse('true'); // true +safeJSONParse('false'); // false +safeJSONParse('null'); // null (valid JSON, parsed value is null) + +// Invalid JSON strings +safeJSONParse('invalid json'); // null +safeJSONParse('{"unclosed": "object"'); // null +safeJSONParse('[1,2,'); // null +safeJSONParse('undefined'); // null +safeJSONParse(''); // null + +// Non-string values +safeJSONParse({ name: 'JinHo' }); // null +safeJSONParse([1, 2, 3]); // null +safeJSONParse(42); // null +safeJSONParse(true); // null +safeJSONParse(null); // null +safeJSONParse(undefined); // null +``` + +### Type-safe parsing with generics + +You can use the generic parameter `T` to describe the expected shape of the parsed JSON value: + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +type User = { + id: number; + name: string; + isAdmin: boolean; +}; + +const raw = '{"id":1,"name":"JinHo","isAdmin":false}'; + +const user = safeJSONParse(raw); +// user: User | null + +if (user !== null) { + // user is narrowed to User here + console.log(user.id); // number + console.log(user.name); // string + console.log(user.isAdmin); // boolean +} +``` + +If no type argument is provided, the return type defaults to `unknown | null`: + +```typescript +const result = safeJSONParse('{"name": "JinHo"}'); +// result: unknown | null +``` + +You can combine `safeJSONParse` with your own type guards (e.g. `isJSONObject`, `isJSONValue`) to further validate the parsed structure. + +### Using with isJSON + +`safeJSONParse` is designed to complement the `isJSON` predicate: + +- Use `isJSON(value)` when you only need to check if a value is a valid JSON string. +- Use `safeJSONParse(value)` when you actually want to parse the JSON string and work with the result. + +```typescript +import { isJSON } from 'es-toolkit/predicate'; +import { safeJSONParse } from 'es-toolkit/util'; + +function parseIfJson(input: unknown) { + if (!isJSON(input)) { + // input is not a valid JSON string + return null; + } + + // input is now typed as string + const parsed = safeJSONParse(input); + + // parsed is the JSON value (or null if something unexpected happens) + return parsed; +} +``` + +### Practical examples + +#### API response parsing + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +type ApiResponse = { + success: boolean; + payload?: unknown; +}; + +function handleApiResponse(responseBody: unknown) { + const data = safeJSONParse(responseBody); + + if (data === null) { + return { + success: false, + error: 'Response body is not valid JSON', + }; + } + + if (!data.success) { + return { + success: false, + error: 'API reported failure', + }; + } + + return { + success: true, + payload: data.payload, + }; +} +``` + +#### User input parsing + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +function tryParseUserConfig(input: unknown) { + const config = safeJSONParse>(input); + + if (config === null) { + return { + isValid: false, + config: null, + error: 'Input is not valid JSON', + }; + } + + return { + isValid: true, + config, + error: null, + }; +} +``` + +::: info Note about "null" string + +When the input is the string `"null"`, `safeJSONParse("null")` returns `null` because that's the actual parsed value. This is indistinguishable from a parse failure purely from the return value, so if you need to distinguish the two cases, combine `isJSON` and `safeJSONParse` or introduce a custom result type. + +::: + +#### Parameters + +- `value` (`unknown`): The value to parse as JSON. + +#### Returns + +(`T | null`): The parsed JSON value if `value` is a string containing valid JSON; otherwise `null`. diff --git a/docs/zh_hans/reference/util/safeJSONParse.md b/docs/zh_hans/reference/util/safeJSONParse.md new file mode 100644 index 000000000..017e89cf2 --- /dev/null +++ b/docs/zh_hans/reference/util/safeJSONParse.md @@ -0,0 +1,177 @@ +# safeJSONParse + +安全地解析 JSON 字符串,不会抛出错误。 + +```typescript +const result = safeJSONParse(value); +``` + +## 用法 + +### `safeJSONParse(value)` + +当您想安全地解析 JSON 字符串时,请使用 `safeJSONParse`。它可以在不冒 `JSON.parse` 异常风险的情况下进行解析。如果值是包含有效 JSON 的字符串,则返回解析后的结果。否则,`safeJSONParse` 返回 `null`。 + +在处理用户输入、松散类型的数据或无法保证值始终是有效 JSON 字符串的不可信源时特别有用。 + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +// 有效的 JSON 字符串 +safeJSONParse('{"name":"JinHo","age":29}'); // { name: "JinHo", age: 29 } +safeJSONParse('[1, 2, 3]'); // [1, 2, 3] +safeJSONParse('"hello world"'); // "hello world" +safeJSONParse('42'); // 42 +safeJSONParse('true'); // true +safeJSONParse('false'); // false +safeJSONParse('null'); // null (有效 JSON,但解析值为 null) + +// 无效的 JSON 字符串 +safeJSONParse('invalid json'); // null +safeJSONParse('{"unclosed": "object"'); // null +safeJSONParse('[1,2,'); // null +safeJSONParse('undefined'); // null +safeJSONParse(''); // null + +// 非字符串值 +safeJSONParse({ name: 'JinHo' }); // null +safeJSONParse([1, 2, 3]); // null +safeJSONParse(42); // null +safeJSONParse(true); // null +safeJSONParse(null); // null +safeJSONParse(undefined); // null +``` + +### 使用泛型进行类型安全解析 + +您可以使用泛型参数 `T` 来描述解析后的 JSON 值的预期形状: + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +type User = { + id: number; + name: string; + isAdmin: boolean; +}; + +const raw = '{"id":1,"name":"JinHo","isAdmin":false}'; + +const user = safeJSONParse(raw); +// user: User | null + +if (user !== null) { + // user 在这里被缩小为 User 类型 + console.log(user.id); // number + console.log(user.name); // string + console.log(user.isAdmin); // boolean +} +``` + +如果未提供类型参数,返回类型默认为 `unknown | null`: + +```typescript +const result = safeJSONParse('{"name": "JinHo"}'); +// result: unknown | null +``` + +您可以将 `safeJSONParse` 与您自己的类型守卫(例如 `isJSONObject`、`isJSONValue`)结合使用,以进一步验证解析后的结构。 + +### 与 isJSON 一起使用 + +`safeJSONParse` 旨在补充 `isJSON` 谓词: + +- 当您只需要检查值是否为有效的 JSON 字符串时,请使用 `isJSON(value)`。 +- 当您实际想要解析 JSON 字符串并使用结果时,请使用 `safeJSONParse(value)`。 + +```typescript +import { isJSON } from 'es-toolkit/predicate'; +import { safeJSONParse } from 'es-toolkit/util'; + +function parseIfJson(input: unknown) { + if (!isJSON(input)) { + // input 不是有效的 JSON 字符串 + return null; + } + + // input 现在被缩小为 string 类型 + const parsed = safeJSONParse(input); + + // parsed 是 JSON 值(或如果发生意外情况则为 null) + return parsed; +} +``` + +### 实用示例 + +#### API 响应解析 + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +type ApiResponse = { + success: boolean; + payload?: unknown; +}; + +function handleApiResponse(responseBody: unknown) { + const data = safeJSONParse(responseBody); + + if (data === null) { + return { + success: false, + error: '响应正文不是有效的 JSON', + }; + } + + if (!data.success) { + return { + success: false, + error: 'API 报告失败', + }; + } + + return { + success: true, + payload: data.payload, + }; +} +``` + +#### 用户输入解析 + +```typescript +import { safeJSONParse } from 'es-toolkit/util'; + +function tryParseUserConfig(input: unknown) { + const config = safeJSONParse>(input); + + if (config === null) { + return { + isValid: false, + config: null, + error: '输入不是有效的 JSON', + }; + } + + return { + isValid: true, + config, + error: null, + }; +} +``` + +::: info 关于 "null" 字符串的说明 + +当输入是字符串 `"null"` 时,`safeJSONParse("null")` 返回 `null`,因为这是实际解析后的值。仅从返回值无法区分这是解析失败还是解析成功。如果您需要区分这两种情况,请结合使用 `isJSON` 和 `safeJSONParse`,或引入自定义结果类型。 + +::: + +#### 参数 + +- `value` (`unknown`): 要解析为 JSON 的值。 + +#### 返回值 + +(`T | null`): 如果 `value` 是包含有效 JSON 的字符串,则返回解析后的 JSON 值;否则返回 `null`。 diff --git a/src/util/index.ts b/src/util/index.ts index 462ef3aef..ece1b2231 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -2,3 +2,4 @@ export { attempt } from './attempt.ts'; export { attemptAsync } from './attemptAsync.ts'; export { invariant } from './invariant.ts'; export { invariant as assert } from './invariant.ts'; +export { safeJSONParse } from './safeJSONParse.ts'; diff --git a/src/util/safeJSONParse.spec.ts b/src/util/safeJSONParse.spec.ts new file mode 100644 index 000000000..9c45fe02a --- /dev/null +++ b/src/util/safeJSONParse.spec.ts @@ -0,0 +1,96 @@ +import { describe, expect, expectTypeOf, it } from 'vitest'; +import { safeJSONParse } from './safeJSONParse'; + +describe('safeJSONParse', () => { + it('returns the parsed value when the input is a valid JSON string', () => { + expect(safeJSONParse('{"name":"JinHo","age":29}')).toEqual({ + name: 'JinHo', + age: 29, + }); + + expect(safeJSONParse('[1,2,3]')).toEqual([1, 2, 3]); + + expect(safeJSONParse('"string"')).toBe('string'); + expect(safeJSONParse('123')).toBe(123); + expect(safeJSONParse('true')).toBe(true); + expect(safeJSONParse('false')).toBe(false); + }); + + it('handles nested objects and arrays', () => { + expect(safeJSONParse('{"user":{"name":"JinHo","age":29},"tags":["admin","user"]}')).toEqual({ + user: { name: 'JinHo', age: 29 }, + tags: ['admin', 'user'], + }); + + expect(safeJSONParse('[[1,2],[3,4]]')).toEqual([ + [1, 2], + [3, 4], + ]); + + expect(safeJSONParse('{"items":[{"id":1,"name":"dj"},{"id":2,"name":"producer"}]}')).toEqual({ + items: [ + { id: 1, name: 'dj' }, + { id: 2, name: 'producer' }, + ], + }); + }); + + it('handles empty objects and arrays', () => { + expect(safeJSONParse('{}')).toEqual({}); + expect(safeJSONParse('[]')).toEqual([]); + expect(safeJSONParse('{"empty":{}}')).toEqual({ empty: {} }); + expect(safeJSONParse('{"items":[]}')).toEqual({ items: [] }); + }); + + it('handles large numbers', () => { + expect(safeJSONParse(String(Number.MAX_SAFE_INTEGER))).toBe(Number.MAX_SAFE_INTEGER); + expect(safeJSONParse(String(Number.MIN_SAFE_INTEGER))).toBe(Number.MIN_SAFE_INTEGER); + expect(safeJSONParse('9007199254740991')).toBe(9007199254740991); + expect(safeJSONParse('-9007199254740991')).toBe(-9007199254740991); + expect(safeJSONParse('0')).toBe(0); + expect(safeJSONParse('-0')).toBe(-0); + }); + it('returns null when the input is "null" (valid JSON but parsed value is null)', () => { + expect(safeJSONParse('null')).toBeNull(); + }); + + it('returns null if the value is not a valid JSON string', () => { + expect(safeJSONParse('invalid json')).toBeNull(); + expect(safeJSONParse('{"unclosed": "object"')).toBeNull(); + expect(safeJSONParse('[1,2,')).toBeNull(); + expect(safeJSONParse('undefined')).toBeNull(); + expect(safeJSONParse('')).toBeNull(); + }); + + it('returns null if the value is not a string', () => { + expect(safeJSONParse(null)).toBeNull(); + expect(safeJSONParse(undefined)).toBeNull(); + expect(safeJSONParse(123)).toBeNull(); + expect(safeJSONParse(true)).toBeNull(); + expect(safeJSONParse({})).toBeNull(); + expect(safeJSONParse([])).toBeNull(); + expect(safeJSONParse(new Date())).toBeNull(); + expect(safeJSONParse(() => {})).toBeNull(); + }); + + // --- Type-level tests (TS inference) --- + + it('infers the generic type parameter', () => { + const result = safeJSONParse<{ name: string }>('{"name":"JinHo"}'); + expectTypeOf(result).toEqualTypeOf<{ name: string } | null>(); + }); + + it('narrows type after null check', () => { + const result = safeJSONParse<{ name: string }>('{"name":"JinHo"}'); + + if (result !== null) { + expectTypeOf(result).toEqualTypeOf<{ name: string }>(); + } + }); + + it('uses the default generic when no type argument is provided', () => { + const result = safeJSONParse('{"name": "JinHo"}'); + + expectTypeOf(result).toEqualTypeOf(); + }); +}); diff --git a/src/util/safeJSONParse.ts b/src/util/safeJSONParse.ts new file mode 100644 index 000000000..883335c48 --- /dev/null +++ b/src/util/safeJSONParse.ts @@ -0,0 +1,61 @@ +/** + * Safely parses a JSON string. + * + * This is a safe wrapper around `JSON.parse()`. If `value` is a string that + * contains valid JSON, the parsed result is returned. Otherwise, this function + * returns `null` instead of throwing an error. + * + * A valid JSON string is one that can be successfully parsed using `JSON.parse()`. + * According to the JSON specification, valid JSON can represent: + * - Objects (with string keys and valid JSON values) + * - Arrays (containing valid JSON values) + * - Strings + * - Numbers + * - Booleans + * - null + * + * String values like `"null"`, `"true"`, `"false"`, and numeric strings + * (e.g., `"42"`) are considered valid JSON and will be parsed to their + * corresponding JavaScript values. + * + * Unlike `isJSON`, this function is not a type guard. Instead, it allows you to + * provide an expected JSON shape via the generic type parameter `T` and returns + * `T | null` depending on whether parsing succeeds. + * + * In other words: + * - Use `isJSON(value)` when you only need to validate a JSON string. + * - Use `safeJSONParse(value)` when you also want to transform it into a value + * without risking a thrown error. + * + * + * @template T The expected JSON value type. + * @param {unknown} value The value to parse as JSON. + * @returns {T | null} The parsed JSON value if `value` is a valid JSON string, otherwise `null`. + * + * @example + * safeJSONParse('{"name":"John","age":30}'); + * // { name: "John", age: 30 } + * + * @example + * safeJSONParse<{ name: string }>('{"name":"John"}'); + * // { name: "John" } + * + * @example + * safeJSONParse('invalid json'); + * // null + * + * @example + * safeJSONParse(42); + * // null + */ +export function safeJSONParse(value: unknown): T | null { + if (typeof value !== 'string') { + return null; + } + + try { + return JSON.parse(value) as T; + } catch { + return null; + } +}