How to parse JSON string in Typescript
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}
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)
Type-safe JSON.parse
You can continue to use JSON.parse
, as TS is a JS superset. There is still a problem left: JSON.parse
returns any
, which undermines type safety. Here are two options for stronger types:
1. User-defined type guards (playground)
Custom type guards are the simplest solution and often sufficient for external data validation:
// 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
function isMyType(o: any): o is MyType {
return "name" in o && "description" in o
}
Extension
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.
2. 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):
-
io-ts
: hasfp-ts
peer dependency, uses functional programming style -
zod
: strives to be more procedural / object-oriented thanio-ts
-
typescript-is
: TS transformer for compiler API, additional wrapper like ttypescript needed -
typescript-json-schema
/ajv
: Create JSON schema from types and validate it withajv
More infos
- Runtime type checking #1573
- Interface type check with Typescript
- TypeScript: validating external data
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"
to123
and re-run the sample. In your console, you will see that the name has been properly converted to the string"123"
.
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