Serialize Undefined Static Props in NextJS
Here's a way to deal with serializing undefined in NextJS getStaticProps function.
NextJS Requirement
The docs say that the props
returned from getStaticProps
must be serializable:
It should be a serializable object so that any props passed, could be serialized with JSON.stringify.
Ok, what does "serializable" mean?
In Vanilla JS, all is well
An undefined
value in vanilla JS works just fine. See this node repl:
> JSON.stringify({ a: undefined })
'{}'
In NextJS, all is errors
But if you do this in getStaticProps
, you're fish bait:
export function getStaticProps() {
return {
props: {
a: undefined
}
}
}
Render that, and you get:
Error serializing `.a`returned from `getStaticProps` in "/my/path".. Reason: `undefined` cannot be serialized as JSON. Please use `null` or omit this value.
JSON is the Reason
Well, Nodejs repl is fine, but NextJS is creating valid JSON. In a JSON validator, that same json gives the error:
Error: Parse error on line 1:
{ "a": undefined }
-------^
Expecting 'STRING', 'NUMBER', 'NULL', 'TRUE', 'FALSE', '{', '[', got 'undefined'
NextJS is the Reason
Well, I mean, valid JSON doesn't want undefineds. But couldn't NextJS take care of this internally? Yes, I think.
There's a discussion happening on Github right now, where a lot of people would like this feature, but I can't see anyone at Vercel biting.
JSON.stringify is the Simplest
In NextJS land, therefore, you'll have to do some of your own data preparation. The easiest way to do this is with the JSON.stringify
method. This is because JSON.stringify
already knows that you don't want undefined
in your JSON, so it strips it out. After it's a string, we'll parse it right back into JavaScript and return that value:
export function getStaticProps() {
return {
props: {
a: JSON.parse(JSON.stringify({ aThing: undefined }))
}
}
}
If you need to do additional processing, like converting Date objects to ISO strings, you can use the JSON.stringify
second param. It's a callback, where you can define the ways that you want certain things serialized. There, you can convert Date
s to string
s, and you're set:
export function getStaticProps() {
return {
props: {
a: serializeObject({ a: undefined })
}
}
}
function serializeObject(obj: unknown): unknown {
return JSON.parse(JSON.stringify(obj, (_key, value) =>
isDate(value) ? value.toISOString() : value
))
}
function isDate() { /* TODO: impl */ }
Recursive Manual Processing
Or, if you're like a grey beard who wants to choose the path of pain, you can try your own implementation. This might be nice for flexibility. Or maybe you're trying to avoid the serialization part, and your just mapping objects.
Here's a reference implementation that converts undefineds to nulls:
/**
* Converts all undefined values in an object to null.
*
* Adapted from: https://stackoverflow.com/questions/50374869/generic-way-to-convert-all-instances-of-null-to-undefined-in-typescript
*/
export function nullifyUndefinedFields<T>(obj: T): RecursivelyReplaceNullWithUndefined<T> {
if (!obj) return null as any
// object check based on: https://stackoverflow.com/a/51458052/6489012
if (obj.constructor.name === 'Object') {
for (let key in obj) {
obj[key] = nullifyUndefinedFields(obj[key]) as any
}
}
return obj as any
}
type RecursivelyReplaceNullWithUndefined<T> = T extends undefined
? null
: {
[K in keyof T]: T[K] extends (infer U)[]
? RecursivelyReplaceNullWithUndefined<U>[]
: RecursivelyReplaceNullWithUndefined<T[K]>
}
And some tests:
import { nullifyUndefinedFields } from './object'
describe('#nullifyUndefinedFields', () => {
test('empty', () => {
expect(nullifyUndefinedFields(undefined)).toEqual(null)
expect(nullifyUndefinedFields(null)).toEqual(null)
})
test('non-object remains untouched', () => {
expect(nullifyUndefinedFields('untouched')).toEqual('untouched')
expect(nullifyUndefinedFields(123)).toEqual(123)
expect(nullifyUndefinedFields(true)).toEqual(true)
})
test('dates remain untouched', () => {
const d = new Date()
expect(nullifyUndefinedFields(d)).toEqual(d)
})
test('arrays remain untouched', () => {
const a = [1, undefined, 2]
expect(nullifyUndefinedFields(a)).toEqual(a)
})
test('top-level undefined', () => {
expect(
nullifyUndefinedFields({
one: 'one',
two: undefined,
}),
).toEqual({
one: 'one',
two: null,
})
})
test('nested undefined', () => {
expect(
nullifyUndefinedFields({
one: 'one',
two: {
three: {
four: undefined,
five: 'five',
},
},
}),
).toEqual({
one: 'one',
two: {
three: {
four: null,
five: 'five',
},
},
})
})
})