Source file jsonschema.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
(** JSON Schema validator supporting drafts 4, 6, 7, 2019-09, and 2020-12 *)

(* Re-export types *)
type draft = Draft.t = Draft4 | Draft6 | Draft7 | Draft2019_09 | Draft2020_12
type schema = Schema.t
type validator = { schema : Schema.t; schemas : Validator.schemas }
type json_pointer = Json_pointer.t
type type_set = Types.t

(* Re-export error types *)
type instance_token = Error.instance_token = Prop of string | Item of int

type instance_location = Error.instance_location = {
  tokens : instance_token list;
}

type error_kind = Error.error_kind =
  | Group
  | Schema of { url : string }
  | Content_schema
  | Property_name of { prop : string }
  | Reference of { kw : string; url : string }
  | Ref_cycle of { url : string; kw_loc1 : string; kw_loc2 : string }
  | False_schema
  | Type of { got : Yojson.Basic.t; want : type_set }
  | Enum of { want : Yojson.Basic.t list }
  | Const of { want : Yojson.Basic.t }
  | Format of { got : Yojson.Basic.t; want : string; err : exn }
  | Min_properties of { got : int; want : int }
  | Max_properties of { got : int; want : int }
  | Additional_properties of { got : string list }
  | Unevaluated_properties of { got : string list }
  | Unevaluated_items of { got : int }
  | Required of { want : string list }
  | Dependency of { prop : string; missing : string list }
  | Dependent_required of { prop : string; missing : string list }
  | Min_items of { got : int; want : int }
  | Max_items of { got : int; want : int }
  | Contains
  | Min_contains of { got : int list; want : int }
  | Max_contains of { got : int list; want : int }
  | Unique_items of { got : int * int }
  | Additional_items of { got : int }
  | Min_length of { got : int; want : int }
  | Max_length of { got : int; want : int }
  | Pattern of { got : string; want : string }
  | Content_encoding of { want : string; err : exn }
  | Content_media_type of { got : bytes; want : string; err : exn }
  | Minimum of { got : float; want : float }
  | Maximum of { got : float; want : float }
  | Exclusive_minimum of { got : float; want : float }
  | Exclusive_maximum of { got : float; want : float }
  | Multiple_of of { got : float; want : float }
  | Not
  | All_of
  | Any_of
  | One_of of (int * int) option

type validation_error = Error.validation_error = {
  schema_url : string;
  instance_location : instance_location;
  kind : error_kind;
  causes : validation_error list;
}

type compile_error = Error.compile_error =
  | Parse_url_error of { url : string; src : exn }
  | Load_url_error of { url : string; src : exn }
  | Unsupported_url_scheme of { url : string }
  | Invalid_meta_schema_url of { url : string; src : exn }
  | Unsupported_draft of { url : string }
  | MetaSchema_cycle of { url : string }
  | Validation_error of { url : string; src : validation_error }
  | Parse_id_error of { loc : string }
  | Parse_anchor_error of { loc : string }
  | Duplicate_id of { url : string; id : string; ptr1 : string; ptr2 : string }
  | Duplicate_anchor of {
      anchor : string;
      url : string;
      ptr1 : string;
      ptr2 : string;
    }
  | Invalid_json_pointer of string
  | Json_pointer_not_found of string
  | Anchor_not_found of { url : string; reference : string }
  | Unsupported_vocabulary of { url : string; vocabulary : string }
  | Invalid_regex of { url : string; regex : string; src : exn }
  | Bug of exn

(* Re-export modules *)
module Types = Types
module Json_pointer = Json_pointer
module Format = Formats
module Content = Content
module Draft = Draft
module Loader = Loader
module Output = Output
module Compiler = Compiler

module Schema = struct
  include Schema

  let validate schema v =
    let schemas = Validator.create_schemas () in
    Validator.insert_schemas schemas [ schema.location ] [ schema ];
    Validator.validate v schema schemas
end

module Validation_error = Validation_error

(* Pretty printing *)
let pp_compile_error (fmt : Stdlib.Format.formatter) err =
  match err with
  | Parse_url_error { url; src } ->
      Stdlib.Format.fprintf fmt "Failed to parse URL %s: %s" url
        (Printexc.to_string src)
  | Load_url_error { url; src } ->
      Stdlib.Format.fprintf fmt "Failed to load URL %s: %s" url
        (Printexc.to_string src)
  | Unsupported_url_scheme { url } ->
      Stdlib.Format.fprintf fmt "Unsupported URL scheme in %s" url
  | Invalid_meta_schema_url { url; src } ->
      Stdlib.Format.fprintf fmt "Invalid meta schema URL %s: %s" url
        (Printexc.to_string src)
  | Unsupported_draft { url } ->
      Stdlib.Format.fprintf fmt "Unsupported draft in %s" url
  | MetaSchema_cycle { url } ->
      Stdlib.Format.fprintf fmt "Meta schema cycle detected in %s" url
  | Validation_error { url; src } ->
      Stdlib.Format.fprintf fmt "Validation error in %s: %s" url
        (Validation_error.to_string src)
  | Parse_id_error { loc } ->
      Stdlib.Format.fprintf fmt "Failed to parse $id at %s" loc
  | Parse_anchor_error { loc } ->
      Stdlib.Format.fprintf fmt "Failed to parse $anchor at %s" loc
  | Duplicate_id { url; id; ptr1; ptr2 } ->
      Stdlib.Format.fprintf fmt "Duplicate $id '%s' in %s at %s and %s" id url
        ptr1 ptr2
  | Duplicate_anchor { anchor; url; ptr1; ptr2 } ->
      Stdlib.Format.fprintf fmt "Duplicate $anchor '%s' in %s at %s and %s"
        anchor url ptr1 ptr2
  | Invalid_json_pointer ptr ->
      Stdlib.Format.fprintf fmt "Invalid JSON pointer: %s" ptr
  | Json_pointer_not_found ptr ->
      Stdlib.Format.fprintf fmt "JSON pointer not found: %s" ptr
  | Anchor_not_found { url; reference } ->
      Stdlib.Format.fprintf fmt "Anchor %s not found in %s" reference url
  | Unsupported_vocabulary { url; vocabulary } ->
      Stdlib.Format.fprintf fmt "Unsupported vocabulary %s in %s" vocabulary url
  | Invalid_regex { url; regex; src } ->
      Stdlib.Format.fprintf fmt "Invalid regex '%s' in %s: %s" regex url
        (Printexc.to_string src)
  | Bug src ->
      Stdlib.Format.fprintf fmt "Internal error: %s" (Printexc.to_string src)

let pp_validation_error (fmt : Stdlib.Format.formatter) err =
  Stdlib.Format.pp_print_string fmt (Validation_error.to_string err)

let pp_validation_error_verbose (fmt : Stdlib.Format.formatter) err =
  Stdlib.Format.pp_print_string fmt (Validation_error.to_string_verbose err)

(* High-level API *)
let validate_file ?draft ~schema json =
  let config =
    match draft with
    | Some d -> { Compiler.default_config with default_draft = d }
    | None -> Compiler.default_config
  in
  let compiler = Compiler.create config in
  match Compiler.compile compiler schema with
  | Error _e ->
      Error
        {
          schema_url = schema;
          instance_location = { tokens = [] };
          kind = Schema { url = "Compilation failed" };
          causes = [];
        }
  | Ok sch ->
      let schemas = Compiler.get_schemas compiler in
      Validator.validate json sch schemas

let validate_strings ?draft ~schema ~json () =
  try
    let json_value = Yojson.Basic.from_string json in
    let schema_value = Yojson.Basic.from_string schema in
    let config =
      match draft with
      | Some d -> { Compiler.default_config with default_draft = d }
      | None -> Compiler.default_config
    in
    let compiler = Compiler.create config in
    match Compiler.compile_json compiler "inline://schema" schema_value with
    | Error _e ->
        Error
          {
            schema_url = "inline://schema";
            instance_location = { tokens = [] };
            kind = Schema { url = "Schema compilation failed" };
            causes = [];
          }
    | Ok sch ->
        let schemas = Compiler.get_schemas compiler in
        Validator.validate json_value sch schemas
  with
  | Failure msg when String.starts_with ~prefix:"get_schema:" msg ->
      (* This is likely an index out of bounds error - let's provide a better message *)
      Error
        {
          schema_url = "inline://schema";
          instance_location = { tokens = [] };
          kind = Schema { url = msg };
          causes = [];
        }
  | e ->
      Error
        {
          schema_url = "inline://schema";
          instance_location = { tokens = [] };
          kind = Schema { url = Printexc.to_string e };
          causes = [];
        }

let create_validator ?draft ?(enable_format_assertions = true)
    ?(enable_content_assertions = true) location =
  let config =
    {
      Compiler.default_config with
      default_draft = Option.value draft ~default:Draft2020_12;
      enable_format_assertions;
      enable_content_assertions;
    }
  in
  let compiler = Compiler.create config in
  match Compiler.compile compiler location with
  | Ok compiled_schema ->
      Ok { schema = compiled_schema; schemas = Compiler.get_schemas compiler }
  | Error e -> Error e

let validate validator json =
  Validator.validate json validator.schema validator.schemas

(* Create validator with custom URL loader *)
let create_validator_with_loader ?draft ?(enable_format_assertions = true)
    ?(enable_content_assertions = true) ~url_loader ~schema () =
  let config =
    {
      Compiler.default_config with
      default_draft = Option.value draft ~default:Draft2020_12;
      enable_format_assertions;
      enable_content_assertions;
      url_loader = Some url_loader;
    }
  in
  let compiler = Compiler.create config in
  match Compiler.compile_json compiler "inline://schema" schema with
  | Ok compiled_schema ->
      Ok { schema = compiled_schema; schemas = Compiler.get_schemas compiler }
  | Error e -> Error e

(* Create validator from JSON schema - for testing *)
let create_validator_from_json ?draft ?(enable_format_assertions = true)
    ?(enable_content_assertions = true) ~schema () =
  let config =
    {
      Compiler.default_config with
      default_draft = Option.value draft ~default:Draft2020_12;
      enable_format_assertions;
      enable_content_assertions;
    }
  in
  let compiler = Compiler.create config in
  match Compiler.compile_json compiler "inline://schema" schema with
  | Ok compiled_schema ->
      Ok { schema = compiled_schema; schemas = Compiler.get_schemas compiler }
  | Error e -> Error e

(* Pre-compiled meta-schema validators *)
let draft4_validator =
  let schema =
    {
      Schema.draft_version = Draft4;
      idx = 0;
      location = "https://json-schema.org/draft-04/schema";
      resource = 0;
      dynamic_anchors = Hashtbl.create 0;
      all_props_evaluated = false;
      all_items_evaluated = false;
      num_items_evaluated = 0;
      boolean = Some true;
      ref_ = None;
      recursive_ref = None;
      recursive_anchor = false;
      dynamic_ref = None;
      dynamic_anchor = None;
      types = Types.empty;
      enum_ = None;
      constant = None;
      not = None;
      all_of = [];
      any_of = [];
      one_of = [];
      if_ = None;
      then_ = None;
      else_ = None;
      format = None;
      min_properties = None;
      max_properties = None;
      required = [];
      properties = Hashtbl.create 0;
      pattern_properties = [];
      property_names = None;
      additional_properties = None;
      dependent_required = [];
      dependent_schemas = [];
      dependencies = [];
      unevaluated_properties = None;
      min_items = None;
      max_items = None;
      unique_items = false;
      min_contains = None;
      max_contains = None;
      contains = None;
      items = None;
      additional_items = None;
      prefix_items = [];
      items2020 = None;
      unevaluated_items = None;
      min_length = None;
      max_length = None;
      pattern = None;
      pattern_string = None;
      content_encoding = None;
      content_media_type = None;
      content_schema = None;
      minimum = None;
      maximum = None;
      exclusive_minimum = None;
      exclusive_maximum = None;
      exclusive_minimum_draft4 = false;
      exclusive_maximum_draft4 = false;
      multiple_of = None;
    }
  in
  let schemas = Validator.create_schemas () in
  Validator.insert_schemas schemas [ schema.location ] [ schema ];
  { schema; schemas }

let draft6_validator =
  let schema =
    {
      draft4_validator.schema with
      draft_version = Draft6;
      location = "https://json-schema.org/draft-06/schema";
    }
  in
  let schemas = Validator.create_schemas () in
  Validator.insert_schemas schemas [ schema.location ] [ schema ];
  { schema; schemas }

let draft7_validator =
  let schema =
    {
      draft4_validator.schema with
      draft_version = Draft7;
      location = "https://json-schema.org/draft-07/schema";
    }
  in
  let schemas = Validator.create_schemas () in
  Validator.insert_schemas schemas [ schema.location ] [ schema ];
  { schema; schemas }

let draft2019_09_validator =
  let schema =
    {
      draft4_validator.schema with
      draft_version = Draft2019_09;
      location = "https://json-schema.org/draft/2019-09/schema";
    }
  in
  let schemas = Validator.create_schemas () in
  Validator.insert_schemas schemas [ schema.location ] [ schema ];
  { schema; schemas }

let draft2020_12_validator =
  let schema =
    {
      draft4_validator.schema with
      draft_version = Draft2020_12;
      location = "https://json-schema.org/draft/2020-12/schema";
    }
  in
  let schemas = Validator.create_schemas () in
  Validator.insert_schemas schemas [ schema.location ] [ schema ];
  { schema; schemas }