Search code examples
typescript

Type-safe JSON Parse


I am trying to write a function

const safeJSONParse = <T>(json: string, default?: T) : T | unknown => {
 try {
  return JSON.parse(json) as T ?? default
 }
 catch (e){
  return default
 }
}

It works with "bad" json strings, but if the type of the object different from T, it still returns the object of the wrong type rather than default value.

Is there a way to make it work that if the returned object is not of type T, the default is returned.


Solution

  • Is there a way to make it work that if the returned object is not of type T, the default is returned.

    Only by adding validation code that checks the result from JSON.parse at runtime. These edges between the untyped world and the typed world always present this challenge.

    There are libraries out there to help with this by having you write your definitions of your shapes (types) using their methods and giving you back both a runtime validation function and a TypeScript type. The one I'm using at the moment is zod but it's not the only one out there.


    FWIW, here's an example using zod, but again, there are other libs or you might just write your own custom code for your types.

    First, you define your schema, and from it you can extract a convenient type alias:

    // A user with a `name` and `email`, both strings
    const userSchema = z.object({
        name: z.string(),
        email: z.string().email(), // Zod has some validators for common things like email addresses
    });
    type User = z.infer<typeof userSchema>;
    

    There, the User type is {name: string; email: string;}.

    Then your function would accept and use a Zod schema:

    const safeJSONParse = <Schema extends ZodTypeAny>(
        json: string,
        schema: Schema,
        defaultValue?: z.infer<Schema>
    ): z.infer<Schema> | undefined => {
        try {
            const result = schema.parse(JSON.parse(json));
            return result;
        } catch (e: any) {
            return defaultValue;
        }
    };
    

    Then you'd use the schema when parsing JSON for a User:

    const user = safeJSONParse(userJson, userSchema);
    if (user) {
        // It's valid
        console.log(user.name);
    }
    

    Playground example