simple-json-schema.mjs 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. function typeOf(value) {
  2. if (value === null) {
  3. return 'null'
  4. }
  5. if (Array.isArray(value)) {
  6. return 'array'
  7. }
  8. if (Number.isInteger(value)) {
  9. return 'integer'
  10. }
  11. return typeof value
  12. }
  13. function jsonPointerDecode(value) {
  14. return value.replace(/~1/g, '/').replace(/~0/g, '~')
  15. }
  16. function resolveRef(rootSchema, ref) {
  17. if (!ref.startsWith('#/')) {
  18. throw new Error(`Unsupported schema ref: ${ref}`)
  19. }
  20. return ref
  21. .slice(2)
  22. .split('/')
  23. .map(jsonPointerDecode)
  24. .reduce((node, key) => {
  25. if (!node || typeof node !== 'object' || !(key in node)) {
  26. throw new Error(`Missing schema ref: ${ref}`)
  27. }
  28. return node[key]
  29. }, rootSchema)
  30. }
  31. function pathFor(path, key) {
  32. return `${path}/${String(key).replace(/~/g, '~0').replace(/\//g, '~1')}`
  33. }
  34. function validateNode(value, schema, rootSchema, path, failures) {
  35. if (!schema || typeof schema !== 'object') {
  36. return
  37. }
  38. if (schema.$ref) {
  39. validateNode(value, resolveRef(rootSchema, schema.$ref), rootSchema, path, failures)
  40. return
  41. }
  42. if ('const' in schema && value !== schema.const) {
  43. failures.push(`${path} must equal ${JSON.stringify(schema.const)}`)
  44. }
  45. if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
  46. failures.push(`${path} must be one of ${schema.enum.map(item => JSON.stringify(item)).join(', ')}`)
  47. }
  48. if (schema.type) {
  49. const accepted = Array.isArray(schema.type) ? schema.type : [schema.type]
  50. const actual = typeOf(value)
  51. const ok = accepted.includes(actual) || (actual === 'integer' && accepted.includes('number'))
  52. if (!ok) {
  53. failures.push(`${path} type ${actual} is not ${accepted.join('|')}`)
  54. return
  55. }
  56. }
  57. if (schema.type === 'object' || (!schema.type && value && typeof value === 'object' && !Array.isArray(value))) {
  58. const objectValue = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
  59. for (const requiredKey of schema.required || []) {
  60. if (!(requiredKey in objectValue)) {
  61. failures.push(`${path} missing required property ${requiredKey}`)
  62. }
  63. }
  64. const properties = schema.properties || {}
  65. for (const [key, childValue] of Object.entries(objectValue)) {
  66. if (properties[key]) {
  67. validateNode(childValue, properties[key], rootSchema, pathFor(path, key), failures)
  68. } else if (schema.additionalProperties === false) {
  69. failures.push(`${path} has unexpected property ${key}`)
  70. } else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {
  71. validateNode(childValue, schema.additionalProperties, rootSchema, pathFor(path, key), failures)
  72. }
  73. }
  74. }
  75. if (schema.type === 'array' && Array.isArray(value) && schema.items) {
  76. value.forEach((item, index) => {
  77. validateNode(item, schema.items, rootSchema, pathFor(path, index), failures)
  78. })
  79. }
  80. }
  81. export function validateJsonSchema(value, schema) {
  82. const failures = []
  83. validateNode(value, schema, schema, '#', failures)
  84. return failures
  85. }