diff --git a/components/utils/json-schema-validator.test.ts b/components/utils/json-schema-validator.test.ts new file mode 100644 index 0000000..842a870 --- /dev/null +++ b/components/utils/json-schema-validator.test.ts @@ -0,0 +1,79 @@ +import { validateJsonSchema } from "./json-schema-validator"; + +describe("validateJsonSchema", () => { + test("should validate JSON data against a correct schema", () => { + const schema = JSON.stringify({ + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" }, + }, + required: ["name", "age"], + }); + + const jsonData = JSON.stringify({ name: "John", age: 30 }); + + const result = validateJsonSchema(schema, jsonData); + expect(result.valid).toBe(true); + expect(result.errors).toEqual([]); + }); + + test("should return errors for invalid JSON data", () => { + const schema = JSON.stringify({ + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" }, + }, + required: ["name", "age"], + }); + + const jsonData = JSON.stringify({ name: "John" }); // Missing age + + const result = validateJsonSchema(schema, jsonData); + expect(result.valid).toBe(false); + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.stringContaining("should have required property 'age'"), + ]) + ); + }); + + test("should return errors for invalid schema", () => { + const schema = "{ invalid JSON schema"; + const jsonData = JSON.stringify({ name: "John", age: 30 }); + + const result = validateJsonSchema(schema, jsonData); + expect(result.valid).toBe(false); + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.stringContaining( + "Expected property name or '}' in JSON at position" + ), + ]) + ); + }); + + test("should return errors for invalid JSON data", () => { + const schema = JSON.stringify({ + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" }, + }, + required: ["name", "age"], + }); + + const jsonData = "{ invalid JSON data"; + + const result = validateJsonSchema(schema, jsonData); + expect(result.valid).toBe(false); + expect(result.errors).toEqual( + expect.arrayContaining([ + expect.stringContaining( + "Expected property name or '}' in JSON at position" + ), + ]) + ); + }); +}); diff --git a/components/utils/json-schema-validator.ts b/components/utils/json-schema-validator.ts new file mode 100644 index 0000000..da212ed --- /dev/null +++ b/components/utils/json-schema-validator.ts @@ -0,0 +1,36 @@ +import Ajv from "ajv"; + +/** + * Validates JSON data against a JSON schema. + * + * @param schema - A JSON schema as a string. + * @param jsonData - JSON data as a string. + * @returns An object with `valid` (boolean) and `errors` (array of error messages) properties. + */ +export const validateJsonSchema = ( + schema: string, + jsonData: string +): { valid: boolean; errors: string[] } => { + try { + const ajv = new Ajv(); + const schemaObj = JSON.parse(schema); + const dataObj = JSON.parse(jsonData); + + const validate = ajv.compile(schemaObj); + const valid = validate(dataObj); + + if (valid) { + return { valid: true, errors: [] }; + } else { + const errors = validate.errors + ? validate.errors.map((error) => `${error.dataPath} ${error.message}`) + : []; + return { valid: false, errors }; + } + } catch (error) { + if (error instanceof Error) { + return { valid: false, errors: [error.message] }; + } + throw error; + } +}; diff --git a/components/utils/tools-list.ts b/components/utils/tools-list.ts index eede9ac..630a9ae 100644 --- a/components/utils/tools-list.ts +++ b/components/utils/tools-list.ts @@ -95,4 +95,10 @@ export const tools = [ "Test and debug your regular expressions in real-time. Provides quick feedback on pattern matching for strings.", link: "/utilities/regex-tester", }, + { + title: "JSON Schema Validator", + description: + "Validate and troubleshoot your JSON data structures in real-time. Ensure your JSON complies with the defined schema for accurate data handling.", + link: "/utilities/json-schema-validator", + }, ]; diff --git a/package-lock.json b/package-lock.json index c47c95d..32daa41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", + "avj": "^0.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", @@ -3970,6 +3971,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/avj": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/avj/-/avj-0.0.0.tgz", + "integrity": "sha512-uYMMuRd+Ux8xH8L1NnAMy+aTsV+UBgQbS3ckRi3ERZWwq5eyXC5D3lC+8w/ZEjHhNpgUUCu61zJktMS8wse+Mg==" + }, "node_modules/axe-core": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.9.1.tgz", diff --git a/package.json b/package.json index 4ca05db..57a83a6 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", + "avj": "^0.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cmdk": "^1.0.0", diff --git a/pages/utilities/json-schema-validator.tsx b/pages/utilities/json-schema-validator.tsx new file mode 100644 index 0000000..fe50ced --- /dev/null +++ b/pages/utilities/json-schema-validator.tsx @@ -0,0 +1,122 @@ +import { useState, useEffect, useCallback } from "react"; +import { Textarea } from "@/components/ds/TextareaComponent"; +import PageHeader from "@/components/PageHeader"; +import { Card } from "@/components/ds/CardComponent"; +import { Label } from "@/components/ds/LabelComponent"; +import Header from "@/components/Header"; +import { CMDK } from "@/components/CMDK"; +import Meta from "@/components/Meta"; +import Ajv from "ajv"; +import CallToActionGrid from "@/components/CallToActionGrid"; + +export default function JsonSchemaValidator() { + const [schema, setSchema] = useState(""); + const [jsonData, setJsonData] = useState(""); + const [result, setResult] = useState(""); + const [resultColor, setResultColor] = useState(""); + + const handleValidation = useCallback(() => { + try { + const ajv = new Ajv(); + const validate = ajv.compile(JSON.parse(schema)); + const valid = validate(JSON.parse(jsonData)); + + if (valid) { + setResult("JSON data is valid against the schema"); + setResultColor("bg-green-100 text-green-800"); // Green color for success + } else { + setResult( + `JSON data is invalid:\n${validate.errors + ?.map((error) => `${error.dataPath} ${error.message}`) + .join("\n")}` + ); + setResultColor("bg-red-100 text-red-800"); // Red color for failure + } + } catch (error) { + if (error instanceof Error) { + setResult(`Error: ${error.message}`); + setResultColor("bg-red-100 text-red-800"); // Red color for error + } + } + }, [schema, jsonData]); + + useEffect(() => { + if (schema && jsonData) { + handleValidation(); + } else { + setResult(""); // Keep the result box empty if fields are not filled + setResultColor(""); // No color if fields are empty + } + }, [schema, jsonData, handleValidation]); + + const handleReset = () => { + setSchema(""); + setJsonData(""); + setResult(""); // Clear the result + setResultColor(""); // Clear the result color + }; + + return ( +
+ +
+ + +
+ +
+ +
+ +
+ +