Is there a way to parse strings as JSON in TypeScript?
For example in JavaScript, we can use JSON.parse()
. Is there a similar function in TypeScript?
I have a JSON object string as follows:
{"name": "Bob", "error": false}
Is there a way to parse strings as JSON in TypeScript?
For example in JavaScript, 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 also have a type for 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);
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:
// 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.
JSON.parse
wrapperCreate 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.
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
: has fp-ts
peer dependency, uses functional programming stylezod
: strives to be more procedural / object-oriented than io-ts
typescript-is
: TS transformer for compiler API, additional wrapper like ttypescript neededtypescript-json-schema
/ajv
: Create JSON schema from types and validate it with ajv
More infos
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.
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.
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:
const value
definition to see that the pop-over shows the correct type."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"
.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
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),
};
You can additionally use libraries that perform type validation of your json, such as Sparkson. They allow you to define a TypeScript class, to which you'd like to parse your response, in your case it could be:
import { Field } from "sparkson";
class Response {
constructor(
@Field("name") public name: string,
@Field("error") public error: boolean
) {}
}
The library will validate if the required fields are present in the JSON payload and if their types are correct. It can also do a bunch of validations and conversions.
JSON.parse
is available in TypeScript, so you can just use it :
JSON.parse('{"name": "Bob", "error": false}') // Returns a value of type 'any'
However, you will often want to parse a JSON object while making sure it matches a certain type, rather than dealing with a value of type any
. In that case, you can define a function such as the following :
function parse_json<TargetType extends Object>(
json: string,
type_definitions: { [Key in keyof TargetType]: (raw_value: any) => TargetType[Key] }
): TargetType {
const raw = JSON.parse(json);
const result: any = {};
for (const key in type_definitions) result[key] = type_definitions[key](raw[key]);
return result;
}
This function takes a JSON string and an object containing individual functions that load each field of the object you are creating. You can use it like so:
const value = parse_json(
'{"name": "Bob", "error": false}',
{ name: String, error: Boolean, }
);
Typescript has a JavaScript runtime because it gets compiled to JS. This means JS objects which are built in as part of the language such as JSON
, Object
, and Math
are also available in TS. Therefore we can just use the JSON.parse
method to parse the JSON string.
const JSONStr = '{"name": "Bob", "error": false}'
// The JSON object is part of the runtime
const parsedObj = JSON.parse(JSONStr);
console.log(parsedObj);
// [LOG]: {
// "name": "Bob",
// "error": false
// }
// The Object object is also part of the runtime so we can use it in TS
const objKeys = Object.keys(parsedObj);
console.log(objKeys);
// [LOG]: ["name", "error"]
The only thing now is that parsedObj is type any
which is generally a bad practice in TS. We can type the object if we are using type guards. Here is an example:
const JSONStr = '{"name": "Bob", "error": false}'
const parsedObj = JSON.parse(JSONStr);
interface nameErr {
name: string;
error: boolean;
}
function isNameErr(arg: any): arg is nameErr {
if (typeof arg.name === 'string' && typeof arg.error === 'boolean') {
return true;
} else {
return false;
}
}
if (isNameErr(parsedObj)) {
// Within this if statement parsedObj is type nameErr;
parsedObj
}
Hey if you do typeof of your json object it turns out to be string which is of typescript. You can read more on that here: Typescript: difference between String and string
So just try this way and it will work-
JSON.parse(String({"name": "Bob", "error": false}))
On my opinion, for data contained in an associative array | Map<string,any>, a bit relaxed (only keys checking) but the easiest way on my opinion (JsDoc version)
(uses an emtpy class instance for type reference and per key matching, returns 'undefined' if parsing or key matching fail )
export class FormCheckError {
constructor(
/** @type {string?}*/ message,
/** @type {string?}*/ oldValue
) {
this.oldValue = oldValue;
this.errorMessage = message;
}
}
/**
* @template T
* @param { Object<keyof T,any>[] } keys2Check
* @param { string } jsonString
* @returns { T | undefined }
*/
export function JsonCheckedParse(keys2Check = [],jsonString = '{}') {
try {
let result = JSON.parse(jsonString)
let resultKeys = Object.keys(result)
if (keys2Check.length !== resultKeys.length) {
return undefined;
}
keys2Check.forEach(function(key) {
if (resultKeys.indexOf(key) == -1) {
return undefined;
}
})
return result;
} catch(e) {
return undefined;
}
}
Usage scenario :
1.Encoding :
import { FormCheckError } from 'types'
...
if (oldValue.length == 0) {
return {
body: JSON.stringify(
new FormCheckError(
"You shouldn't post empty entries.",
oldValue
)
)
}
}
...
2.Decoding:
import { FormCheckError } from 'types'
import { JsonCheckedParse } from 'json'
...
/** @type {import('./$types').ActionData} */ // Sveltekit stuff
export let form; // response received from 'ssr-side.js'
// will be undefined in case of type mismatch beetween encoded & decoded
/** @type {FormCheckError | undefined}*/
let checkError = JsonCheckedParse(
Object.keys(new FormCheckError()), // an empty class instance for knowing its keys
form?.body || '{}' // the JSON to parse and check for type
)
...
if (checkError?.errorMessage) {
console.log(String(checkError.errorMessage))
}
...
Yes it's a little tricky in TypeScript but you can do the below example like this
let decodeData = JSON.parse(`${jsonResponse}`);