How to parse JSON string in Typescript

395,123

Solution 1

Typescript is (a superset of) javascript, so you just use JSON.parse as you would in javascript:

let obj = JSON.parse(jsonString);

Only that in typescript you can have a type to the resulting object:

interface MyObj {
    myString: string;
    myNumber: number;
}

let obj: MyObj = JSON.parse('{ "myString": "string", "myNumber": 4 }');
console.log(obj.myString);
console.log(obj.myNumber);

(code in playground)

Solution 2

Type-safe JSON.parse

You can continue to use JSON.parse, as TypeScript is a superset of JavaScript:

This means you can take any working JavaScript code and put it in a TypeScript file without worrying about exactly how it is written.

There is a problem left: JSON.parse returns any, which undermines type safety (don't use any).

Here are three solutions for stronger types, ordered by ascending complexity:

1. User-defined type guards

Playground

// For example, you expect to parse a given value with `MyType` shape
type MyType = { name: string; description: string; }

// Validate this value with a custom type guard (extend to your needs)
function isMyType(o: any): o is MyType {
  return "name" in o && "description" in o
}

const json = '{ "name": "Foo", "description": "Bar" }';
const parsed = JSON.parse(json);
if (isMyType(parsed)) {
  // do something with now correctly typed object
  parsed.description
} else { 
// error handling; invalid JSON format 
}

isMyType is called a type guard. Its advantage is, that you get a fully typed object inside truthy if branch.

2. Generic JSON.parse wrapper

Playground

Create a generic wrapper around JSON.parse, which takes one type guard as input and returns the parsed, typed value or error result:

const safeJsonParse = <T>(guard: (o: any) => o is T) => 
  (text: string): ParseResult<T> => {
    const parsed = JSON.parse(text)
    return guard(parsed) ? { parsed, hasError: false } : { hasError: true }
  }

type ParseResult<T> =
  | { parsed: T; hasError: false; error?: undefined }
  | { parsed?: undefined; hasError: true; error?: unknown }

Usage example:

const json = '{ "name": "Foo", "description": "Bar" }';
const result = safeJsonParse(isMyType)(json) // result: ParseResult<MyType>
if (result.hasError) {
  console.log("error :/")  // further error handling here
} else {
  console.log(result.parsed.description) // result.parsed now has type `MyType`
}

safeJsonParse might be extended to fail fast or try/catch JSON.parse errors.

3. External libraries

Writing type guard functions manually becomes cumbersome, if you need to validate many different values. There are libraries to assist with this task - examples (no comprehensive list):


More infos

Solution 3

If you want your JSON to have a validated Typescript type, you will need to do that validation work yourself. This is nothing new. In plain Javascript, you would need to do the same.

Validation

I like to express my validation logic as a set of "transforms". I define a Descriptor as a map of transforms:

type Descriptor<T> = {
  [P in keyof T]: (v: any) => T[P];
};

Then I can make a function that will apply these transforms to arbitrary input:

function pick<T>(v: any, d: Descriptor<T>): T {
  const ret: any = {};
  for (let key in d) {
    try {
      const val = d[key](v[key]);
      if (typeof val !== "undefined") {
        ret[key] = val;
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      throw new Error(`could not pick ${key}: ${msg}`);
    }
  }
  return ret;
}

Now, not only am I validating my JSON input, but I am building up a Typescript type as I go. The above generic types ensure that the result infers the types from your "transforms".

In case the transform throws an error (which is how you would implement validation), I like to wrap it with another error showing which key caused the error.

Usage

In your example, I would use this as follows:

const value = pick(JSON.parse('{"name": "Bob", "error": false}'), {
  name: String,
  error: Boolean,
});

Now value will be typed, since String and Boolean are both "transformers" in the sense they take input and return a typed output.

Furthermore, the value will actually be that type. In other words, if name were actually 123, it will be transformed to "123" so that you have a valid string. This is because we used String at runtime, a built-in function that accepts arbitrary input and returns a string.

You can see this working here. Try the following things to convince yourself:

  • Hover over the const value definition to see that the pop-over shows the correct type.
  • Try changing "Bob" to 123 and re-run the sample. In your console, you will see that the name has been properly converted to the string "123".

Solution 4

There is a great library for it ts-json-object

In your case you would need to run the following code:

import {JSONObject, required} from 'ts-json-object'

class Response extends JSONObject {
    @required
    name: string;

    @required
    error: boolean;
}

let resp = new Response({"name": "Bob", "error": false});

This library will validate the json before parsing

Solution 5

Use app.quicktype.io to safely parse JSON in TypeScript. More on this shortly. JSON.parse() returns type any and is sufficient in the "happy path" but can lead to errors related to type-safety downstream which defeats the purpose of TypeScript. For example:

interface User {
  name: string,
  balance: number
}

const json = '{"name": "Bob", "balance": "100"}' //note the string "100"
const user:User = JSON.parse(json)

const newBalance = user.balance + user.balance * 0.05 //should be 105 after interest
console.log(newBalance ) //but it ends up as 1005 which is clearly wrong

So let quicktype do the heavy lifting and generate the code. Copy and paste the string below in quicktype.

{
  "name": "Bob",
  "balance": 100
}

Make sure to choose TypeScript as the language and enable "Verify JSON.parse results at runtime"

Now we can defensively handle exception (if any) at the time of parsing and prevent errors from happening downstream.

import { Convert, User } from "./user";

const json =
  '{"firstName": "Kevin", "lastName": "Le", "accountBalance": "100"}';

try {
  const user = Convert.toUser(json);
  console.log(user);
} catch (e) {
  console.log("Handle error", e);
}

user.ts is the file generated by quicktype.

// To parse this data:
//
//   import { Convert, User } from "./file";
//
//   const user = Convert.toUser(json);
//
// These functions will throw an error if the JSON doesn't
// match the expected interface, even if the JSON is valid.

export interface User {
    name:    string;
    balance: number;
}

// Converts JSON strings to/from your types
// and asserts the results of JSON.parse at runtime
export class Convert {
    public static toUser(json: string): User {
        return cast(JSON.parse(json), r("User"));
    }

    public static userToJson(value: User): string {
        return JSON.stringify(uncast(value, r("User")), null, 2);
    }
}

function invalidValue(typ: any, val: any, key: any = ''): never {
    if (key) {
        throw Error(`Invalid value for key "${key}". Expected type ${JSON.stringify(typ)} but got ${JSON.stringify(val)}`);
    }
    throw Error(`Invalid value ${JSON.stringify(val)} for type ${JSON.stringify(typ)}`, );
}

function jsonToJSProps(typ: any): any {
    if (typ.jsonToJS === undefined) {
        const map: any = {};
        typ.props.forEach((p: any) => map[p.json] = { key: p.js, typ: p.typ });
        typ.jsonToJS = map;
    }
    return typ.jsonToJS;
}

function jsToJSONProps(typ: any): any {
    if (typ.jsToJSON === undefined) {
        const map: any = {};
        typ.props.forEach((p: any) => map[p.js] = { key: p.json, typ: p.typ });
        typ.jsToJSON = map;
    }
    return typ.jsToJSON;
}

function transform(val: any, typ: any, getProps: any, key: any = ''): any {
    function transformPrimitive(typ: string, val: any): any {
        if (typeof typ === typeof val) return val;
        return invalidValue(typ, val, key);
    }

    function transformUnion(typs: any[], val: any): any {
        // val must validate against one typ in typs
        const l = typs.length;
        for (let i = 0; i < l; i++) {
            const typ = typs[i];
            try {
                return transform(val, typ, getProps);
            } catch (_) {}
        }
        return invalidValue(typs, val);
    }

    function transformEnum(cases: string[], val: any): any {
        if (cases.indexOf(val) !== -1) return val;
        return invalidValue(cases, val);
    }

    function transformArray(typ: any, val: any): any {
        // val must be an array with no invalid elements
        if (!Array.isArray(val)) return invalidValue("array", val);
        return val.map(el => transform(el, typ, getProps));
    }

    function transformDate(val: any): any {
        if (val === null) {
            return null;
        }
        const d = new Date(val);
        if (isNaN(d.valueOf())) {
            return invalidValue("Date", val);
        }
        return d;
    }

    function transformObject(props: { [k: string]: any }, additional: any, val: any): any {
        if (val === null || typeof val !== "object" || Array.isArray(val)) {
            return invalidValue("object", val);
        }
        const result: any = {};
        Object.getOwnPropertyNames(props).forEach(key => {
            const prop = props[key];
            const v = Object.prototype.hasOwnProperty.call(val, key) ? val[key] : undefined;
            result[prop.key] = transform(v, prop.typ, getProps, prop.key);
        });
        Object.getOwnPropertyNames(val).forEach(key => {
            if (!Object.prototype.hasOwnProperty.call(props, key)) {
                result[key] = transform(val[key], additional, getProps, key);
            }
        });
        return result;
    }

    if (typ === "any") return val;
    if (typ === null) {
        if (val === null) return val;
        return invalidValue(typ, val);
    }
    if (typ === false) return invalidValue(typ, val);
    while (typeof typ === "object" && typ.ref !== undefined) {
        typ = typeMap[typ.ref];
    }
    if (Array.isArray(typ)) return transformEnum(typ, val);
    if (typeof typ === "object") {
        return typ.hasOwnProperty("unionMembers") ? transformUnion(typ.unionMembers, val)
            : typ.hasOwnProperty("arrayItems")    ? transformArray(typ.arrayItems, val)
            : typ.hasOwnProperty("props")         ? transformObject(getProps(typ), typ.additional, val)
            : invalidValue(typ, val);
    }
    // Numbers can be parsed by Date but shouldn't be.
    if (typ === Date && typeof val !== "number") return transformDate(val);
    return transformPrimitive(typ, val);
}

function cast<T>(val: any, typ: any): T {
    return transform(val, typ, jsonToJSProps);
}

function uncast<T>(val: T, typ: any): any {
    return transform(val, typ, jsToJSONProps);
}

function a(typ: any) {
    return { arrayItems: typ };
}

function u(...typs: any[]) {
    return { unionMembers: typs };
}

function o(props: any[], additional: any) {
    return { props, additional };
}

function m(additional: any) {
    return { props: [], additional };
}

function r(name: string) {
    return { ref: name };
}

const typeMap: any = {
    "User": o([
        { json: "name", js: "name", typ: "" },
        { json: "balance", js: "balance", typ: 0 },
    ], false),
};
Share:
395,123

Related videos on Youtube

ssd20072
Author by

ssd20072

Updated on March 10, 2022

Comments

  • ssd20072
    ssd20072 about 2 years

    Is there a way to parse strings as JSON in Typescript.
    Example: In JS, we can use JSON.parse(). Is there a similar function in Typescript?

    I have a JSON object string as follows:

    {"name": "Bob", "error": false}
    
    • nnnnnn
      nnnnnn almost 8 years
      Can't you just use JSON.parse()?
    • sigalor
      sigalor almost 8 years
      On its homepage, it says that "TypeScript is a typed superset of JavaScript that compiles to plain JavaScript". The JSON.parse() function should be usable like normal.
    • ssd20072
      ssd20072 almost 8 years
      I'm using the Atom text editor and when I do a JSON.parse, I get the error: Argument of type '{}' is not assignable to parameter of type 'string'
    • Nitzan Tomer
      Nitzan Tomer almost 8 years
      This is a very basic question, and it might seem trivial to some but it's a valid question none the less, and an equivalent can't be found in SO (I haven't) so there's no real reason why not to keep the question running, and in my opinion shouldn't be down voted as well.
    • Nitzan Tomer
      Nitzan Tomer almost 8 years
      @SanketDeshpande When you use JSON.parse you get an object as a result and not a string (see my answer for more). If you want to turn an object into a string then you need to use JSON.stringify instead.
    • ssd20072
      ssd20072 almost 8 years
      @NitzanTomer Thank you so much for your reply. And I did search the web to check whether JSON.parse() is allowed in Typescript and I did not find any answers. Hence, I posted here.
    • speciesUnknown
      speciesUnknown over 5 years
      Actually it is not a simple question for 2 reasons. Firstly, JSON.parse() doesnt return the same kind of object - it will match some of the interface but anything intelligent, such as accessors, will not be present. Furthermore, surely we want SO to be where people go when they google stuff?
    • AUSTX_RJL
      AUSTX_RJL almost 5 years
      github.com/windhandel/angular-http-deserializer worked for me to get instantiated TypeScript objects from JSON, I tried json2typescript, but it seemed to have trouble with nested Types
  • David Portabella
    David Portabella over 6 years
    how to validate that the input is valid (type-checking, one of the purposes of typescript)? replacing the input '{ "myString": "string", "myNumber": 4 }' by '{ "myString": "string", "myNumberBAD": 4 }' will not fail, and obj.myNumber will return undefined.
  • Nitzan Tomer
    Nitzan Tomer over 6 years
    @DavidPortabella You can't have type-checking on the content of a string. It's a runtime issue, and type checking is for compile time
  • David Portabella
    David Portabella over 6 years
    ok. how can i validate that a typescript obj satisfies its interface at runtime? that is, that myNumber is not undefined in this example. for instance, in Scala Play, you would use Json.parse(text).validate[MyObj]. playframework.com/documentation/2.6.x/ScalaJson how can you do the same in typescript (maybe there is an external library to do so?)?
  • Nitzan Tomer
    Nitzan Tomer over 6 years
    @DavidPortabella There's no way to do that, not easily, because at runtime MyObj doesn't exist. There are plenty of other threads in SO about this subject, for example: Check if an object implements an interface at runtime with TypeScript
  • David Portabella
    David Portabella over 6 years
    ok, thanks. everyday i am more convinced about using scalajs.
  • Nitzan Tomer
    Nitzan Tomer over 6 years
    @DavidPortabella Well, scalajs won't be able to do that without adding a bunch of code to the runtime, so you'll end up with more code than what you wrote. You can write ts/js code to do that as well, and without abstractions, your code will probably be more specific to your needs.
  • Manu Chadha
    Manu Chadha over 5 years
    @DavidPortabella - I haven't used Scalajs but I know Scala provides strong type checking. What is your experience on Scalajs? I also wonder how helpful using scalajs would be considering that frameworks like Angular use typescript. Does any famous JS framework use scalajs?
  • speciesUnknown
    speciesUnknown over 5 years
    JSON.Parse doesn't return a real reference, but rather, an anonymous object which matches most of the propeties. This technique will fail with this object - there will be no b on the constructed object: class Something {public a:string; public get b() { return a;} }
  • Nitzan Tomer
    Nitzan Tomer over 5 years
    @gburton In this example, there's no class, so your point is irrelevant. It is indeed a real reference, but not to an instance of a class. The OP asked about how to parse a json encoded string into a js object with a defined type, my answer references that.
  • speciesUnknown
    speciesUnknown over 5 years
    @NitzanTomer Maybe I was too hasty - what if we expanded your answer? I was just annoyed becuase I wasted 20 minutes on the mystery of the missing accessor before realising that
  • Nitzan Tomer
    Nitzan Tomer over 5 years
    @gburton That's a completely different question though, and therefore requires a different answer, see this answer of mine for a scenario like I believe you're describing: stackoverflow.com/a/43314819/942852
  • Nitzan Tomer
    Nitzan Tomer over 5 years
    @gburton BTW, I replied in the same thread to another question, exactly what you wrote me (stackoverflow.com/questions/43314791/…) but there the question did include the class instance, unlike here.
  • speciesUnknown
    speciesUnknown over 5 years
    You're probably right, but the only way somebody new to typescript would know that is by finding it by accident, maybe you could put a disclaimer and a link to your other answer? an instance of a class is a typescript object, and the question title doesn't specify "native javascript object".
  • Pranav A.
    Pranav A. over 5 years
    I love this answer, it is a really clean answer :)
  • Joncom
    Joncom almost 4 years
    you gave an example, "if name were actually 123, it will be transformed to "123". This seems to be incorrect. My value is coming back {name: 123.. not {name:"123".. when I copy paste all your code exactly and make that change.
  • chowey
    chowey almost 4 years
    Weird, it works for me. Try it here: typescriptlang.org/play/index.html (using 123 instead of "Bob").
  • lovasoa
    lovasoa almost 4 years
    I dont think you need to define a Transformed type. You can just use Object. type Descriptor<T extends Object> = { ... };
  • chowey
    chowey almost 4 years
    Thanks @lovasoa, you are correct. The Transformed type is totally unnecessary. I've updated the answer accordingly.
  • ford04
    ford04 almost 4 years
    You should mention, that you are the main contributor of above library.
  • xuiqzy
    xuiqzy over 3 years
    If you actually want to validate that the JSON Object has the correct types, you would not want 123 to be automatically converted to a string "123", since it is a number in the JSON object.
  • chowey
    chowey over 3 years
    Correct. If you want to validate the JSON object, your transformer should check, e.g. instead of String use (v: any) => { if (typeof v === 'string') return v; else throw new TypeError("wrong type"); }
  • zeropaper
    zeropaper about 3 years
    Does not answer the question properly, bad code/example (no point in ${jsonResponse})
  • AlexMorley-Finch
    AlexMorley-Finch over 2 years
    How to handle invalid json?