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 Dates to strings, 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',
        },
      },
    })
  })
})