From b7fcc802037b29ed148da63b46e0f7e3c5a08227 Mon Sep 17 00:00:00 2001 From: Guillaume Date: Fri, 22 Nov 2024 15:06:23 +0100 Subject: [PATCH] Add json schema validator --- components/editors/base-editor.tsx | 2 +- components/editors/json-editor.tsx | 24 +-- .../editors/json-schema-content-editor.tsx | 63 ++++++++ components/editors/json-schema-editor.tsx | 49 +++++++ components/editors/json-utils.tsx | 30 ++++ package-lock.json | 34 +++-- package.json | 2 + pages/tools/index.tsx | 11 ++ pages/tools/json-schema-validator.tsx | 137 ++++++++++++++++++ .../illustrations/json-schema-validator.svg | 17 +++ 10 files changed, 336 insertions(+), 33 deletions(-) create mode 100644 components/editors/json-schema-content-editor.tsx create mode 100644 components/editors/json-schema-editor.tsx create mode 100644 components/editors/json-utils.tsx create mode 100644 pages/tools/json-schema-validator.tsx create mode 100644 public/images/illustrations/json-schema-validator.svg diff --git a/components/editors/base-editor.tsx b/components/editors/base-editor.tsx index 7319756d..e682d7b2 100644 --- a/components/editors/base-editor.tsx +++ b/components/editors/base-editor.tsx @@ -113,7 +113,7 @@ const BaseEditor: FunctionComponent<{ )} {!error && showValidMsg && ( -
+
{lang.toUpperCase()} is valid!
)} diff --git a/components/editors/json-editor.tsx b/components/editors/json-editor.tsx index 4a3617fa..3298cc61 100644 --- a/components/editors/json-editor.tsx +++ b/components/editors/json-editor.tsx @@ -1,27 +1,7 @@ -import { json, jsonParseLinter } from '@codemirror/lang-json'; -import { linter } from '@codemirror/lint'; -import { Text } from '@uiw/react-codemirror'; +import { json } from '@codemirror/lang-json'; import { FunctionComponent } from 'react'; import BaseEditor from './base-editor'; - -const jsonLinter = linter(jsonParseLinter(), { delay: 100 }); - -function getErrorPosition( - error: SyntaxError, - doc: Text -): { line?: number; column?: number; position?: number } { - let match; - if ((match = error.message.match(/at position (\d+)/))) - return { position: Math.min(+match[1], doc.length) }; - if ((match = error.message.match(/at line (\d+) column (\d+)/))) - return { - position: Math.min(doc.line(+match[1]).from + +match[2] - 1, doc.length) - }; - - return { - position: 1 - }; -} +import { getErrorPosition, jsonLinter } from './json-utils'; const JsonEditor: FunctionComponent<{ value: string; diff --git a/components/editors/json-schema-content-editor.tsx b/components/editors/json-schema-content-editor.tsx new file mode 100644 index 00000000..ea495fe2 --- /dev/null +++ b/components/editors/json-schema-content-editor.tsx @@ -0,0 +1,63 @@ +import { json } from '@codemirror/lang-json'; +import { FunctionComponent, useState } from 'react'; +import BaseEditor from './base-editor'; +import { getErrorPosition, initAjv, jsonLinter } from './json-utils'; + +/** + * Contains a JSON object to be validated against a JSON schema + * + * @param props + * @returns + */ +const JsonSchemaContentEditor: FunctionComponent<{ + value: string; + schema: string; + showValidMsg?: boolean; + showErrors?: boolean; + onValueChange?: (value: string) => void; +}> = function (props) { + const [hideGoToLine, setHideGoToLine] = useState(false); + const ajvInstance = initAjv(); + + return ( + { + try { + JSON.parse(currentValue); + + const validate = ajvInstance.compile(JSON.parse(props.schema)); + const isValid = validate(JSON.parse(currentValue)); + + if (!isValid) { + setHideGoToLine(true); + + return { + message: `Validation errors:\n${validate.errors + .slice(0, 6) + .map( + (error) => + `${error.instancePath ? error.instancePath.replace(/^\//, '') + ' ' : 'JSON '}${error.message}` + ) + .join('\n')}` + }; + } + + return null; + } catch (error) { + setHideGoToLine(false); + + return { + message: `JSON error: ${error.message}`, + ...getErrorPosition(error, viewUpdate.state.doc) + }; + } + }} + hideGoToLine={hideGoToLine} + {...props} + /> + ); +}; + +export default JsonSchemaContentEditor; diff --git a/components/editors/json-schema-editor.tsx b/components/editors/json-schema-editor.tsx new file mode 100644 index 00000000..e0c78824 --- /dev/null +++ b/components/editors/json-schema-editor.tsx @@ -0,0 +1,49 @@ +import { json } from '@codemirror/lang-json'; +import { FunctionComponent } from 'react'; +import BaseEditor from './base-editor'; +import { getErrorPosition, initAjv, jsonLinter } from './json-utils'; + +/** + * Contains a JSON schema + * + * @param props + * @returns + */ +const JsonSchemaEditor: FunctionComponent<{ + value: string; + showValidMsg?: boolean; + showErrors?: boolean; + onValueChange?: (value: string) => void; +}> = function (props) { + const ajvInstance = initAjv(); + + return ( + { + try { + JSON.parse(currentValue); + + try { + ajvInstance.compile(JSON.parse(currentValue)); + } catch (error) { + return { + message: `Schema error: ${error.message.replace('strict mode: ', '')}`, + ...getErrorPosition(error, viewUpdate.state.doc) + }; + } + return null; + } catch (error) { + return { + message: `JSON error: ${error.message}`, + ...getErrorPosition(error, viewUpdate.state.doc) + }; + } + }} + {...props} + /> + ); +}; + +export default JsonSchemaEditor; diff --git a/components/editors/json-utils.tsx b/components/editors/json-utils.tsx new file mode 100644 index 00000000..546f28a0 --- /dev/null +++ b/components/editors/json-utils.tsx @@ -0,0 +1,30 @@ +import { jsonParseLinter } from '@codemirror/lang-json'; +import { linter } from '@codemirror/lint'; +import { Text } from '@uiw/react-codemirror'; +import Ajv from 'ajv'; +import addFormats from 'ajv-formats'; + +export const jsonLinter = linter(jsonParseLinter(), { delay: 100 }); + +export function getErrorPosition( + error: SyntaxError, + doc: Text +): { line?: number; column?: number; position?: number } { + let match; + if ((match = error.message.match(/at position (\d+)/))) + return { position: Math.min(+match[1], doc.length) }; + if ((match = error.message.match(/at line (\d+) column (\d+)/))) + return { + position: Math.min(doc.line(+match[1]).from + +match[2] - 1, doc.length) + }; + + return { + position: 1 + }; +} + +export function initAjv() { + const ajv = new Ajv({ allErrors: true }); + addFormats(ajv); + return ajv; +} diff --git a/package-lock.json b/package-lock.json index f00a3f44..953c0481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,8 @@ "@tanstack/react-query": "^5.8.3", "@uiw/codemirror-theme-nord": "^4.21.21", "@uiw/react-codemirror": "^4.21.21", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "bootstrap": "^5.3.2", "date-fns": "^3.6.0", "eslint-config-next": "^14.0.2", @@ -3452,7 +3454,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3479,10 +3480,9 @@ } }, "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dependencies": { "ajv": "^8.0.0" }, @@ -7609,6 +7609,23 @@ "npm": ">5.0.0" } }, + "node_modules/exegesis/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -7790,8 +7807,7 @@ "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" }, "node_modules/fast-url-parser": { "version": "1.1.3", @@ -10492,8 +10508,7 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -14202,7 +14217,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index 26020324..ca25d542 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,8 @@ "@tanstack/react-query": "^5.8.3", "@uiw/codemirror-theme-nord": "^4.21.21", "@uiw/react-codemirror": "^4.21.21", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", "bootstrap": "^5.3.2", "date-fns": "^3.6.0", "eslint-config-next": "^14.0.2", diff --git a/pages/tools/index.tsx b/pages/tools/index.tsx index fb4e986c..281bdf15 100644 --- a/pages/tools/index.tsx +++ b/pages/tools/index.tsx @@ -187,6 +187,17 @@ const tools = [ } ], imageSrc: '/images/illustrations/uuid-generator.svg' + }, + { + title: 'JSON schema validator', + description: 'Validate your JSON data against a JSON schema online', + links: [ + { + src: '/tools/json-schema-validator/', + text: 'Generate' + } + ], + imageSrc: '/images/illustrations/json-schema-validator.svg' } ]; diff --git a/pages/tools/json-schema-validator.tsx b/pages/tools/json-schema-validator.tsx new file mode 100644 index 00000000..9810d068 --- /dev/null +++ b/pages/tools/json-schema-validator.tsx @@ -0,0 +1,137 @@ +import { FunctionComponent, useState } from 'react'; +import JsonSchemaContentEditor from '../../components/editors/json-schema-content-editor'; +import JsonSchemaEditor from '../../components/editors/json-schema-editor'; +import Hero from '../../components/hero'; +import Meta from '../../components/meta'; +import ToolsCta from '../../components/tools-cta'; +import Layout from '../../layout/layout'; + +const JsonSchemaValidator: FunctionComponent = function () { + const [jsonSchema, setJsonSchema] = useState(`{ + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "email": { + "type": "string", + "format": "email" + }, + "password": { + "type": "string", + "minLength": 6 + } + }, + "required": ["username", "email", "password"] +}`); + const [jsonContent, setJsonContent] = useState(`{ + "username": "John", + "email": "john@example.org", + "password": "123456" +}`); + + return ( + + + +
+
+
+
+ { + try { + setJsonSchema(value); + } catch (error) {} + }} + /> +
+ +
+ +
+ { + try { + setJsonContent(value); + } catch (error) {} + }} + /> +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+

About this tool

+

+ This tool allows you to validate your JSON data against a JSON + schema. It uses the [ajv](https://ajv.js.org/) library to + perform the validation. +

+

+ To use this tool, paste your JSON schema in the left editor and + your JSON data in the right editor. The tool will automatically + validate your JSON data against the schema and display any + errors. +
+ It will also display errors if your JSON schema is invalid. +

+

What is JSON?

+

+ JSON (JavaScript Object Notation) is a lightweight{' '} + data interchange format. It is easy to read and + write and easy to parse and generate using code. +
+ It is based on a{' '} + subset of the JavaScript programming language. +

+

+ JSON is a text format that is completely + language independent but uses conventions that are familiar to + programmers of many languages like C, C++, C#, Java or + JavaScript. These properties make JSON an ideal data-interchange + language. +

+

What is a JSON schema?

+

+ A JSON schema is a JSON document that defines the structure of + your JSON data. It allows you to specify the type of each field, + the required fields, and any constraints on the data. +

+

+ The JSON schema format is defined by the{' '} + JSON Schema standard. +

+

+ JSON schemas are useful for validating JSON data, generating + documentation, and providing a contract for APIs. +

+
+
+
+
+
+ ); +}; + +export default JsonSchemaValidator; diff --git a/public/images/illustrations/json-schema-validator.svg b/public/images/illustrations/json-schema-validator.svg new file mode 100644 index 00000000..98024772 --- /dev/null +++ b/public/images/illustrations/json-schema-validator.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + +