Skip to content

Commit

Permalink
Native JSON support for response serializing/deserializing long int (…
Browse files Browse the repository at this point in the history
…BigInt)
  • Loading branch information
Rendez committed Feb 20, 2024
1 parent 15124d1 commit ddb9eff
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 85 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,18 @@ const body = arrayRequestBody([{ item: 1 }], { param: 2 })
// body type is { item: number }[] & { param: number }
```

### JSON response serializing/deserializing long int (BigInt)

Stringifying and parsing big numeric values could be problematic. JSON.parse will coerce large numeric values and JSON.stringify will throw an error: `Uncaught TypeError: Do not know how to serialize a BigInt` in such cases.

To circumvent this issue, `BigInt` is replaced with `rawJSON` while serializing and JSON.parse relies on [source text access](https://github.com/tc39/proposal-json-parse-with-source) to transform big numeric values into `BigInt` automatically.

#### JS engine support

Support for this is conditional, the TC39 proposal has reached staged 3 and has shipped with Chrome by default already, with the rest of modern browsers [soon to follow](https://github.com/tc39/proposal-json-parse-with-source/issues/15#issue-664090651) with their corresponding releases. For Node.js support, at least Node 20 is required to be run with the next harmony flag `node --harmony-json-parse-with-source`, until it is switched by default in future versions.

If you rely on the precision of big number in responses, or are sending big numeric values, make sure your JavaScript environment supports it.

---

Happy fetching! 👍
44 changes: 40 additions & 4 deletions src/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,41 @@ import {
_TypedFetch,
TypedFetch,
} from './types.js'
import { JSONParse, JSONStringify } from './json-with-bigint.js'

declare global {
interface JSON {
rawJSON(jsonStr: string): {
rawJSON: string
}
}
}

type JSONReviverReplacer = (
key: string,
val: any,
context?: {
/** Only provided if a value is primitive */
source?: any
},
) => any

let bigintReviver: JSONReviverReplacer | undefined
let bigintReplacer: JSONReviverReplacer | undefined

if ('rawJSON' in JSON) {
bigintReviver = function (_key, val, context) {
if (typeof val === 'number' && !Number.isSafeInteger(val)) {
return BigInt(context?.source)
}
return val
}
bigintReplacer = function (_key, val) {
if (typeof val === 'bigint') {
return JSON.rawJSON(String(val))
}
return val
}
}

const sendBody = (method: Method) =>
method === 'post' ||
Expand Down Expand Up @@ -96,7 +130,9 @@ function getBody(method: Method, payload: unknown): CustomRequestInit['body'] {
return
}
const body =
payload instanceof FormData ? payload : JSONStringify(payload as any)
payload instanceof FormData
? payload
: JSON.stringify(payload, bigintReplacer)
// if delete don't send body if empty
return method === 'delete' && body === '{}' ? undefined : body
}
Expand Down Expand Up @@ -156,11 +192,11 @@ async function getResponseData(response: Response) {
const contentType = response.headers.get('content-type')
const responseText = await response.text()
if (contentType && contentType.includes('application/json')) {
return JSONParse(responseText)
return JSON.parse(responseText, bigintReviver)
}

try {
return JSONParse(responseText)
return JSON.parse(responseText, bigintReviver)
} catch (e) {
return responseText
}
Expand Down
81 changes: 0 additions & 81 deletions src/json-with-bigint.ts

This file was deleted.

0 comments on commit ddb9eff

Please sign in to comment.