diff --git a/lib/util/schema-utils.spec.ts b/lib/util/schema-utils.spec.ts
index 10af9db538eb20c4e0a2c1f546c2052b41068b82..f8ae991b26b53addf3406735f166e1ab9d578ae7 100644
--- a/lib/util/schema-utils.spec.ts
+++ b/lib/util/schema-utils.spec.ts
@@ -1,26 +1,32 @@
 import { z } from 'zod';
-import * as schema from './schema-utils';
+import {
+  looseArray,
+  looseRecord,
+  looseValue,
+  parseJson,
+  safeParseJson,
+} from './schema-utils';
 
 describe('util/schema-utils', () => {
   describe('looseArray', () => {
     it('parses array', () => {
-      const s = schema.looseArray(z.string());
+      const s = looseArray(z.string());
       expect(s.parse(['foo', 'bar'])).toEqual(['foo', 'bar']);
     });
 
     it('handles non-array', () => {
-      const s = schema.looseArray(z.string());
+      const s = looseArray(z.string());
       expect(s.parse({ foo: 'bar' })).toEqual([]);
     });
 
     it('drops wrong items', () => {
-      const s = schema.looseArray(z.string());
+      const s = looseArray(z.string());
       expect(s.parse(['foo', 123, null, undefined, []])).toEqual(['foo']);
     });
 
     it('runs callback for wrong elements', () => {
       let called = false;
-      const s = schema.looseArray(z.string(), () => {
+      const s = looseArray(z.string(), () => {
         called = true;
       });
       expect(s.parse(['foo', 123, 'bar'])).toEqual(['foo', 'bar']);
@@ -29,7 +35,7 @@ describe('util/schema-utils', () => {
 
     it('runs callback for non-array', () => {
       let called = false;
-      const s = schema.looseArray(z.string(), () => {
+      const s = looseArray(z.string(), () => {
         called = true;
       });
       expect(s.parse('foobar')).toEqual([]);
@@ -39,23 +45,23 @@ describe('util/schema-utils', () => {
 
   describe('looseRecord', () => {
     it('parses record', () => {
-      const s = schema.looseRecord(z.string());
+      const s = looseRecord(z.string());
       expect(s.parse({ foo: 'bar' })).toEqual({ foo: 'bar' });
     });
 
     it('handles non-record', () => {
-      const s = schema.looseRecord(z.string());
+      const s = looseRecord(z.string());
       expect(s.parse(['foo', 'bar'])).toEqual({});
     });
 
     it('drops wrong items', () => {
-      const s = schema.looseRecord(z.string());
+      const s = looseRecord(z.string());
       expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({ foo: 'foo' });
     });
 
     it('runs callback for wrong elements', () => {
       let called = false;
-      const s = schema.looseRecord(z.string(), () => {
+      const s = looseRecord(z.string(), () => {
         called = true;
       });
       expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({ foo: 'foo' });
@@ -64,7 +70,7 @@ describe('util/schema-utils', () => {
 
     it('runs callback for non-record', () => {
       let called = false;
-      const s = schema.looseRecord(z.string(), () => {
+      const s = looseRecord(z.string(), () => {
         called = true;
       });
       expect(s.parse('foobar')).toEqual({});
@@ -74,22 +80,76 @@ describe('util/schema-utils', () => {
 
   describe('looseValue', () => {
     it('parses value', () => {
-      const s = schema.looseValue(z.string());
+      const s = looseValue(z.string());
       expect(s.parse('foobar')).toBe('foobar');
     });
 
     it('falls back to null wrong value', () => {
-      const s = schema.looseValue(z.string());
+      const s = looseValue(z.string());
       expect(s.parse(123)).toBeNull();
     });
 
     it('runs callback for wrong elements', () => {
       let called = false;
-      const s = schema.looseValue(z.string(), () => {
+      const s = looseValue(z.string(), () => {
         called = true;
       });
       expect(s.parse(123)).toBeNull();
       expect(called).toBeTrue();
     });
   });
+
+  describe('parseJson', () => {
+    it('parses json', () => {
+      const res = parseJson('{"foo": "bar"}', z.object({ foo: z.string() }));
+      expect(res).toEqual({ foo: 'bar' });
+    });
+
+    it('throws on invalid json', () => {
+      expect(() =>
+        parseJson('{"foo": "bar"', z.object({ foo: z.string() }))
+      ).toThrow(SyntaxError);
+    });
+
+    it('throws on invalid schema', () => {
+      expect(() =>
+        parseJson('{"foo": "bar"}', z.object({ foo: z.number() }))
+      ).toThrow(z.ZodError);
+    });
+  });
+
+  describe('safeParseJson', () => {
+    it('parses json', () => {
+      const res = safeParseJson(
+        '{"foo": "bar"}',
+        z.object({ foo: z.string() })
+      );
+      expect(res).toEqual({ foo: 'bar' });
+    });
+
+    it('returns null on invalid json', () => {
+      const res = safeParseJson('{"foo": "bar"', z.object({ foo: z.string() }));
+      expect(res).toBeNull();
+    });
+
+    it('returns null on invalid schema', () => {
+      const res = safeParseJson(
+        '{"foo": "bar"}',
+        z.object({ foo: z.number() })
+      );
+      expect(res).toBeNull();
+    });
+
+    it('runs callback on invalid json', () => {
+      const callback = jest.fn();
+      safeParseJson('{"foo": "bar"', z.object({ foo: z.string() }), callback);
+      expect(callback).toHaveBeenCalledWith(expect.any(SyntaxError));
+    });
+
+    it('runs callback on invalid schema', () => {
+      const callback = jest.fn();
+      safeParseJson('{"foo": "bar"}', z.object({ foo: z.number() }), callback);
+      expect(callback).toHaveBeenCalledWith(expect.any(z.ZodError));
+    });
+  });
 });
diff --git a/lib/util/schema-utils.ts b/lib/util/schema-utils.ts
index 781cca9d8ea98162870c1c44c4438ab40e44b596..d8996cb2a9e7ff4d5f45d1ed0c1af75580f89587 100644
--- a/lib/util/schema-utils.ts
+++ b/lib/util/schema-utils.ts
@@ -90,3 +90,27 @@ export function looseValue<T, U extends z.ZodTypeDef, V>(
     : nullableSchema.catch(null);
   return schemaWithFallback;
 }
+
+export function parseJson<
+  T = unknown,
+  Schema extends z.ZodType<T> = z.ZodType<T>
+>(input: string, schema: Schema): z.infer<Schema> {
+  const parsed = JSON.parse(input);
+  return schema.parse(parsed);
+}
+
+export function safeParseJson<
+  T = unknown,
+  Schema extends z.ZodType<T> = z.ZodType<T>
+>(
+  input: string,
+  schema: Schema,
+  catchCallback?: (e: SyntaxError | z.ZodError) => void
+): z.infer<Schema> | null {
+  try {
+    return parseJson(input, schema);
+  } catch (err) {
+    catchCallback?.(err);
+    return null;
+  }
+}