Skip to content

Instantly share code, notes, and snippets.

@Pietro-Putelli
Created October 27, 2024 15:25
Show Gist options
  • Save Pietro-Putelli/2b07a046e20e4b62ee682203e1a96b9d to your computer and use it in GitHub Desktop.
Save Pietro-Putelli/2b07a046e20e4b62ee682203e1a96b9d to your computer and use it in GitHub Desktop.
const validateSchema = (data, schema, path = "", errors = []) => {
switch (schema.type) {
case "string":
if (typeof data !== "string") {
errors.push(`Expected type "string" but got ${typeof data} at ${path}`);
}
break;
case "number":
if (typeof data !== "number") {
errors.push(`Expected type "number" but got ${typeof data} at ${path}`);
}
break;
case "boolean":
if (typeof data !== "boolean") {
errors.push(
`Expected type "boolean" but got ${typeof data} at ${path}`
);
}
break;
case "object":
if (typeof data !== "object" || Array.isArray(data)) {
errors.push(`Expected type "object" but got ${typeof data} at ${path}`);
} else {
// 1. Check that all required props are in the data object.
const requiredProps = schema.required;
if (requiredProps) {
requiredProps.forEach((prop) => {
if (!(prop in data)) {
errors.push(
`Missing required property ${
path == "" ? "data" : path
}.${prop}`
);
}
});
}
// 2. Check that all props in data are allowed by the schema.
const invalidDataProps = Object.keys(data)
.map((key) => {
if (!(key in schema.properties)) {
return key;
}
})
.filter(Boolean);
if (invalidDataProps.length != 0) {
invalidDataProps.forEach((key) => {
errors.push(`Invalid key at ${path == "" ? "data" : path}.${key}`);
});
}
if (schema.properties) {
for (const key in schema.properties) {
const dataField = data?.[key];
if (dataField != undefined) {
const newPath = `${path ? path + "." : ""}${key}`;
validateSchema(
dataField,
schema.properties[key],
newPath,
errors
);
}
}
}
}
break;
case "array":
if (!Array.isArray(data)) {
errors.push(
`Expected type "array" but got ${typeof data} at path ${path}`
);
} else {
for (const index in data) {
// 1. Validate each single item in the array
validateSchema(
data[index],
schema.items,
`${path == "" ? "data" : path}[${index}]`,
errors
);
}
}
break;
case "oneOf":
const oneOfErrors = schema.oneOf.map((subSchema) => {
const subErrors = [];
validateSchema(
data,
subSchema,
`oneOf.${path == "" ? data : ""}.${subSchema.type}`,
subErrors
);
return subErrors;
});
// Check if at least one schema validation passed with no errors
if (oneOfErrors.every((errorList) => errorList.length > 0)) {
errors.push(
`Value at oneOf.${path} does not match any allowed schemas`
);
}
break;
case "merge":
// Merge sub-schemas
const mergedSchema = schema.merge.reduce(
(acc, subSchema) => {
if (subSchema.type === "object") {
// Merge properties
if (subSchema.properties) {
acc.properties = { ...acc.properties, ...subSchema.properties };
}
// Merge required fields
if (subSchema.required) {
acc.required = [...(acc.required || []), ...subSchema.required];
}
} else {
// For non-object types, store them in a list to validate against
acc.nonObjectSchemas = acc.nonObjectSchemas || [];
acc.nonObjectSchemas.push(subSchema);
}
return acc;
},
{ type: "object", properties: {}, required: [] }
);
if (
mergedSchema.nonObjectSchemas &&
mergedSchema.nonObjectSchemas.length > 0
) {
// Validate data against non-object schemas
mergedSchema.nonObjectSchemas.forEach((subSchema) => {
validateSchema(data, subSchema, path, errors);
});
} else {
// Validate data against the merged object schema
validateSchema(data, mergedSchema, path, errors);
}
break;
}
return errors;
};
/* Testing */
// Test Case 1: "object" type
const schema_1 = {
type: "object",
properties: {
name: { type: "string" },
surname: { type: "string" },
address: {
type: "object",
properties: {
value: { type: "string" },
caps: { type: "array", items: { type: "string" } },
country: {
type: "oneOf",
oneOf: [{ type: "string" }, { type: "number" }],
},
},
required: ["caps"],
},
keywords: {
type: "array",
items: {
type: "string",
},
},
},
required: ["name", "surname", "address"],
};
const obj_1 = {
name: "Mario",
surname: "Rossi",
address: {
caps: ["00100", "00200"],
country: "Italy",
},
keywords: ["developer", "javascript"],
};
const errors1 = validateSchema(obj_1, schema_1);
console.log("Errors for obj_1 with schema_1:", errors1);
// Test Case 2: "array" type
const schema_2 = {
type: "array",
items: {
type: "number",
},
};
const obj_2 = [1, 2, "3"];
const errors2 = validateSchema(obj_2, schema_2);
console.log("\nErrors for obj_2 with schema_2:", errors2);
// Test Case 3: "oneOf" type
const schema_3 = {
type: "oneOf",
oneOf: [
{
type: "string",
},
{
type: "number",
},
],
};
const obj_3 = "Hello World";
const errors3 = validateSchema(obj_3, schema_3);
console.log("\nErrors for obj_3 with schema_3:", errors3);
// Test Case 4: "merge" type
const mergeSchema = {
type: "merge",
merge: [
{
type: "object",
properties: {
value: {
type: "oneOf",
oneOf: [{ type: "string" }, { type: "number" }],
},
},
required: ["value"],
},
{
type: "object",
properties: {
numbers: {
type: "array",
items: { type: "number" },
},
},
required: ["numbers"],
},
{
type: "object",
properties: {
name: { type: "string" },
},
required: ["name"],
},
],
};
const data1 = {
value: 42,
numbers: [1, 2, 3],
name: "Luigi",
};
const errors4 = validateSchema(data1, mergeSchema);
console.log("\nErrors for data1 with mergeSchema:", errors4);
// Additional test for "merge" with invalid data
const invalidData = {
value: "true", // Invalid type
numbers: [1, 2], // Invalid item in array
};
const errors5 = validateSchema(invalidData, mergeSchema);
console.log("\nErrors for invalidData with mergeSchema:", errors5);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment