JSON Schema for Julia
CorvusJsonSchema is the Corvus JSON Schema evaluator for Julia: draft 4, 6, 7, 2019-09 and 2020-12. It is a port of the .NET runtime evaluator (see Runtime Evaluator), and gives the same results and annotations. It needs Julia 1.10 or later and has no dependencies.
A schema is compiled once into a node graph and fail-fast plans: each plan holds only the checks its subschema needs, and an object's keywords are fused into one pass over its properties. Any number of instances can then be validated against it.
- Conformant. Passes the whole JSON-Schema-Test-Suite (required, optional and
optional/format, every draft) exceptdraft4/optional/zeroTerminatedFloats.json, and all of its annotation tests. - Fast. See Performance.
- Results and annotations. Basic, Detailed and Verbose results, and annotations, the same as every other Corvus implementation.
- Allocation-free validation. In the steady state, validating a parsed document, or JSON text, allocates nothing.
The exceptions are an asserted
regex,idn-hostnameoridn-emailformat, ahostnameoremailwith anxn--label, a custom format (which is given a copy of the string), and amultipleOfwhose divisor has more than 18 significant digits. - Ready when loaded. The evaluator is compiled into the package image, so the first validation in a process does not wait for Julia's compiler.
Install
pkg> add CorvusJsonSchema
Validate
using CorvusJsonSchema
validator = compile_schema("""{
"type": "object",
"properties": {"id": {"type": "integer", "minimum": 1}},
"required": ["id"]
}""")
isvalid(validator, """{"id": 3}""") # true
isvalid(validator, """{"id": 0}""") # false
A Validator is not modified by validation and is safe to use from several tasks at once. A schema that is not JSON
throws a ParseError, and one that cannot be compiled (an unresolvable reference, an invalid pattern, a pattern that
is refused) throws a CompileError.
JSON text
isvalid takes JSON text as a string or as a vector of UTF-8 bytes. It parses the text into buffers the validator
reuses and validates it in place, so in the steady state it allocates nothing. To validate the same text more than
once, parse it into a Document: the UTF-8 text and one flat array of values, with strings read in place where they
have no escapes.
validator = compile_schema("""{"type": "array", "items": {"type": "integer"}}""")
# Parse once, validate any number of times.
document = parse_document("[1, 2, 3]")
isvalid(validator, document) # true
# JSON text is parsed into buffers the validator reuses.
isvalid(validator, """[1, "two"]""") # false
isvalid(validator, Vector{UInt8}("[4, 5]")) # true
isvalid reports text that is not JSON, and a schema that recursed in place beyond the maximum depth, as invalid.
validate throws for those instead: a ParseError, or a DepthExceededError.
validator = compile_schema("""{"type": "object"}""")
try
validate(validator, """{"id": 3""")
catch err
println(sprint(showerror, err))
end
# invalid JSON at offset 8: unexpected end of input
# isvalid reports text that is not JSON as invalid.
isvalid(validator, """{"id": 3""") # false
Options
compile_schema and compile_schema_uri take keyword arguments after the schema:
| Keyword | Meaning |
|---|---|
default_dialect |
The dialect of a schema without $schema (default Draft202012). |
assert_format |
true asserts format, false never does. With nothing, the default, the schema's vocabularies decide. |
assert_format_in_legacy_drafts |
With assert_format at nothing, assert format in drafts 4 to 7 too. |
assert_content |
Assert contentEncoding and contentMediaType in draft 7 (default true). |
formats |
Custom formats, as a dictionary from the name to a function from the string (or a number's JSON text) to whether it is valid. |
resolver |
A function from an absolute URI to the document (a Document, JSON text, or nothing), for remote references. The standard metaschemas are built in. |
base_uri |
The base URI of the root document. |
entry_point |
A subschema to validate against, such as #/$defs/item. |
max_depth |
The deepest the evaluator recurses in place (default 128). |
pattern_match_limit |
The limit on the time of one match of a pattern or patternProperties regular expression, in seconds (default 1). nothing turns it off. A match that reaches it throws a PatternMatchError. See Limits. |
A schema that has a $ in it is easiest to write as a raw string.
item = parse_document("""{"type": "string", "format": "even"}""")
validator = compile_schema(raw"""{"$defs": {"item": {"$ref": "item.json"}}}""";
default_dialect=Draft201909, # for schemas without $schema (default Draft202012)
assert_format=true, # true or false. With nothing the vocabularies decide
formats=Dict("even" => value -> iseven(length(value))),
resolver=uri -> uri == "https://example.com/item.json" ? item : nothing,
base_uri="https://example.com/root.json",
entry_point=raw"#/$defs/item",
max_depth=128,
pattern_match_limit=1) # seconds for one match of a pattern. With nothing there is no limit
isvalid(validator, "\"four\"") # true
isvalid(validator, "\"three\"") # false
Results and annotations
validator = compile_schema("""{"properties": {"id": {"type": "integer"}}, "required": ["name"]}""")
instance = parse_document("""{"id": "seven"}""")
collector = ResultsCollector(Detailed)
evaluate(validator, instance, collector) # false
for r in results(collector)
if r.evaluation_location != "" && r.message != ""
println(r.evaluation_location, " at \"", r.document_evaluation_location, "\": ", r.message)
end
end
# /properties/id at "/id": The value was expected to match the subschema.
# /properties/id/type at "/id": The value was expected to be of type 'integer'
# /required at "/name": Required property not present 'name'
Each row is a SchemaResult with is_match, message, evaluation_location, schema_evaluation_location and
document_evaluation_location. Basic records the failures without messages, Detailed adds the messages, and
Verbose records every keyword, passing ones and annotations included.
validator = compile_schema("""{
"title": "Person",
"properties": {"name": {"title": "Name", "type": "string"}}
}""")
collector = ResultsCollector(Verbose)
evaluate(validator, """{"name": "Ada"}""", collector) # true
# Instance location, then keyword, then schema location, then the value as JSON text.
found = collect_annotations(collector)
found[""]["title"]["#"] # "Person"
found["/name"]["title"]["#/properties/name"] # "Name"
Patterns
pattern and patternProperties have ECMA-262 semantics. The common shapes of pattern are matched with no regular
expression engine. Any other pattern is translated into one that the PCRE2 library Julia bundles matches with the
same meaning, with every character class written from the package's own Unicode 17 data. The answers are those of V8
on every Julia version, with one exception for Julia 1.10.
What is exact:
- Every construct but the two below, for every pattern, whether it is valid with the
uflag or only without it. That includes a backreference to a group that has not taken part, the groups of a repeated body, an iteration that matches the empty string, and a backreference inside a lookbehind to a group at its end. - A pattern that is valid only without the
uflag (one with an identity escape such as\-or\&) matches the UTF-16 code units of the string, as ECMA-262 says. A character beyond the Basic Multilingual Plane is two code units, so^\-?.$does not match U+10400, which^.$matches. A quantifier after such a character in the pattern repeats its second half,\u{...}is the letterucounted, and a class range from one such character to another is an error. A string with such a character is written again as code units for the match, into a buffer the package keeps, so the match allocates nothing in the steady state. - A count of one character or class, whatever the count.
a{1000000000}and^\p{L}{1,300000}$are run. PCRE2 counts to 65,535, and a larger count is written as blocks of the character that the pattern calls. - What is a pattern, which is what ECMA-262 says. That is V8's verdict but in three places.
\p{sc=Hrkt}(the Script valueKatakana_Or_Hiragana, which Unicode lists and no character has) is a pattern that matches nothing, and V8 13.6 does not know the name. A count whose first number is the greater is an error however large the numbers, soa{2147483648,2147483647}is one, which V8 13.6 takes. The four scripts Unicode 17 added (Sidetic,Tolong_Siki,Beria_Erfe,Tai_Yo) are names here and not in V8 13.6.\kin a class is an error in a pattern with a named group, as in V8:(?<n>a)[\k]. - A case-insensitive backreference such as
(.)(?i:\1), in every pattern that is valid with theuflag, with Julia 1.13. PCRE2 makes the comparison, by Unicode simple case folding, as ECMA-262 does with theuflag. It compares one character at a time. V8 13.6 compares the whole text by full case folding there, and says that^(..)\1$with theiflag matches U+00DF,S,s, U+1E9E. The package follows ECMA-262 and says that it does not. - A backreference to a group of a lookbehind, where the lookbehind has a fixed length, where the group is at a
fixed distance from its end (
(?<=a*(x))-\1), and where the lookbehind is a term of the pattern itself and starts with a repetition of a body of a fixed length ((?<=([abc])+)x\1) or with alternatives of fixed lengths ((?<=(ab|a)x)-\1).
A pattern that is valid ECMA-262 and cannot be written for PCRE2 with exactly the same meaning is refused: compiling
the schema throws a CompileError that names the construct. It is never run with another meaning. These are:
- A case-insensitive backreference in a pattern that is only valid without the
uflag (one with an Annex B form such as\&), where the group can hold a character that ECMA-262 compares differently there than PCRE2 does, such ask,s, or a character beyond the Basic Multilingual Plane that has a case variant.^(.)(?i:\1)\&?$is refused. No pattern that is valid with theuflag is refused for this. - A backreference to a group inside a lookbehind of no fixed length, where the lookbehind is not a term of the
pattern itself (
(?:(?<=(a)+)x\1)+), or where the group holds what depends on the order in which ECMA-262 tries the ways through the lookbehind from its end ((?<=(a*)(x+))-\1). - A backreference inside a lookbehind to a group that the lookbehind matches before it, where the group is not in
an end of the lookbehind of a fixed length (
(?<=\1(a+))). - A pattern PCRE2 does not compile for a reason that is not its size, which no pattern is known to be. (A pattern that is valid and too large is not refused in this way. See Limits.)
The lookbehind exception. A pattern that is valid with the u flag is run, and answers as V8 does, with one
named exception. Two uses of a lookbehind need it to be matched backwards, as ECMA-262 matches it, which PCRE2 cannot
do, and they are refused with an error that says which:
- a group captured inside a lookbehind of no fixed length and read after it, outside the forms that are written,
such as
(?<=(a*)(x+))-\1("a backreference to a group inside a lookbehind of no fixed length, where the group holds what only matching the lookbehind backwards can tell"); - a backreference inside a lookbehind to a group of it that is not in an end of it of a fixed length, such as
(?<=\1(a+))x("a backreference inside a lookbehind to a group that the lookbehind matches before it").
A lookbehind with no group that a backreference reads is never refused for this. A pattern over a limit of size is the only other such pattern that does not compile, and its error is one of its own.
Limits
A schema and an instance may both come from outside, so a pattern and a match of it have limits. A pattern or a match over one is an error of its own, which names the limit and its value. It is never an answer.
The limit on a match. One match of a pattern or patternProperties regular expression against one string is
limited to one second. The keyword pattern_match_limit of compile_schema sets the limit, in seconds, and
nothing turns it off, for schemas and instances that are trusted. A match that reaches the limit is given up and
the validation throws a PatternMatchError, from isvalid as from validate and evaluate, with a collector or
without one, and from compile_schema when the match was of a pattern against a property name the schema
declares. It is never reported as a string that matches or does not. Validate inside try when the schema or the
instance is not trusted. The field limit of the error says which limit was reached.
limit |
What was reached | Value |
|---|---|---|
:time |
The time of a match | pattern_match_limit, 1 second unless it is set |
:memory |
The memory of a match, in PCRE2's interpreter | 64 MiB |
:steps |
The steps of a match that could not be timed | See below |
validator = compile_schema(raw"""{"pattern": "^(?:a|a)+$"}"""; pattern_match_limit=0.05)
isvalid(validator, "\"aaaa\"") # true
# Each a is matched two ways, so there are 2^40 ways to find that this string does not match.
hostile = "\"" * "a"^40 * "b\""
outcome = try
isvalid(validator, hostile)
catch error
error isa PatternMatchError ? error.limit : rethrow()
end
outcome # time
# A pattern over a limit of size does not compile. It is not said to be invalid.
too_large = try
compile_schema(raw"""{"pattern": "(?:(?:a{1000}){1000}){1000}"}""")
catch error
error isa PatternTooLargeError ? error.valid : rethrow()
end
too_large # true
PCRE2 has no clock. It counts steps, and its count does not bound time: it counts the steps of each place a match is tried from apart, and one step may read the whole string. So a match is run in two stages. The first is the code PCRE2 compiles, with a number of steps worked out from the pattern and the length of the string so that it cannot take long (a twentieth to a half of the limit at worst, and microseconds for a match that is not hostile). Nearly every match ends there, at the cost it had. A match that uses those steps is run again from the start by code that PCRE2 calls back from before every item of the pattern, which reads the clock, so the match ends with its answer or at the limit. That code was measured 8 to 30 times slower on patterns that repeat a group, which is why it is not the code of every match. On the probe of hostile patterns the package keeps (257 patterns of at most 1,000 characters on strings of at most 100,000), a match that was given up took 1.13 seconds at most with the limit at one second, where 25 of them did not end in a minute before.
What the limit costs, and what it does not bound:
- A long string and a pattern that repeats a group. The steps of the first stage are fewer the longer the
string, so a string of some tens of thousands of characters matched by a pattern that repeats a group, such as
^(?:[a-z0-9]+\.)*[a-z0-9]+$, or searched for a pattern that is not anchored, is run by the slower code. It answers as it did, some 5 to 30 times slower on such strings. A class or a character under*or+(^[a-z]+$,^\p{L}+$) is not slower, on a string of any length.pattern_match_limit=nothingruns every match as before. - Memory. A repeated group needs memory for each time it has matched. PCRE2's JIT code has a stack of 1 MiB,
and a match that fills it is run by PCRE2's interpreter, which may take 64 MiB. Past that the match is a
PatternMatchErrorwhoselimitis:memory, with any limit on time or none:^(?:ab|cd)*$reaches it on a string of some 500,000 characters. A class of many ranges under*or+no longer needs that memory:^\p{L}+$threw on a string of 150,000 letters, and answers on a million. - With no limit on time a match still has a limit.
pattern_match_limit=nothingturns the clock off. It does not make a match unlimited: PCRE2 counts the steps of a match, it cannot be told to count none, and 4,294,967,295 (2^32 - 1) for each place a match is tried from is the most it can be told to allow. A match that takes them from one place is given up, and the validation throws aPatternMatchErrorwhoselimitis:stepsand whosecodeis -47, which is PCRE2'sPCRE2_ERROR_MATCHLIMIT, fromisvalidas fromvalidateandevaluate. Itsmessageis "the match took the 4294967295 steps that are the most PCRE2 allows (PCRE2 error -47, match limit exceeded)". It is never an answer.^(?:a|a)+$reaches it on fortyaand ab, after 8 seconds with Julia 1.13 and 9 with Julia 1.10. The number is not a bound on time: one step may read the whole string, and a pattern that is not anchored has the steps again at each place it is tried from. The limit on memory stands with the clock off too. Sonothingis for schemas and instances that are trusted, and is not a way to wait for every answer. - A pattern too large to be timed. PCRE2, as Julia builds it, holds a compiled pattern of at most 64 KiB, and
the code that reads the clock is larger than the code that does not. A pattern PCRE2 holds only without it, such
as an alternation of 3,000 words, is run, and its matches end in their first stage. One that does not is given
up there, sooner than its limit, with
limitat:steps. - The faster matchers. A pattern the package matches without PCRE2 (a literal, an anchored class sequence, a list of alternatives) reads the string a bounded number of times and has no limit.
The limits on the size of a pattern. A pattern over one of these does not compile. The error is a
PatternTooLargeError, and never the CompileError of a pattern that is not valid. Its field valid says whether
the whole pattern was read before the limit was met. The message is then "is a valid ECMA-262 regular expression,
and is too large", and otherwise "is too large to read", as nothing is known of the rest of the pattern.
| Limit | Value | The message ends |
|---|---|---|
| The length of the pattern | 1,048,576 bytes of UTF-8 | "its length is over the limit of 1048576 bytes" |
| The nesting of its groups | 200 | "its groups nest over the limit of 200 deep" |
| Its terms and alternatives | 131,072 | "it has over the limit of 131072 terms and alternatives" |
| The translation PCRE2 is given | 1,048,576 bytes | "its translation is over the limit of 1048576 bytes" |
| The steps to write the translation | 524,288 | "its translation takes over the limit of 524288 steps to write" |
| A repetition that is written out twice | 65,536 bytes | "a repetition that is written out is over the limit of 65536 bytes" |
| Lookbehinds of no fixed length | 252 | "it has over the limit of 252 lookbehinds of no fixed length" |
| What PCRE2 holds | 64 KiB compiled, 250 nested parentheses, 65,535 groups | "PCRE2 does not hold it, and says ..." |
| The compiled pattern and those of its lookbehinds | 1,048,576 bytes together | "PCRE2 does not hold it, and says ..." |
The limit a pattern of a few hundred characters meets is PCRE2's 64 KiB. PCRE2 writes a counted group out once for
each time, so a large count of a group of several characters passes it: (?:ab){6552} is run and (?:ab){6553} is
not, (?:a{1000}){1000} is run and (?:(?:a{1000}){1000}){1000} is not. A counted character or class is never too
large (\p{L}{1000000000} is run). A repetition of what can match the empty string inside another, such as
(?:(?:a*)*)*, was written twice at each level and is written once, so a hundred levels are run where twelve were
too large. With a minimum of one or more ((?:(?:a*)+)+) it is written twice still, and some twelve levels are too
large. The JIT code of a pattern is at most 4 MiB, and nothing is refused for that: a pattern that would pass it,
such as one of several hundred \p{L}, is run by PCRE2's interpreter. On the probe, a pattern of at most 1,000
characters is translated and compiled in 0.4 seconds at most and keeps 2.5 MB at most. A pattern is read in time
in proportion to its length: a group is compared with the last group of its name, where it was compared with every
one, so 20,000 groups of one name inside 150 groups (160 KB, which PCRE2 does not hold) are refused in 27 ms, where
they took a minute. The patterns a process
has compiled are kept for the next schema that has them, up to 4,096 patterns and 32 MiB.
The formats. A string that format asserts is a string of the instance. Every check reads its bytes with a
scanner written for the package, and none is a regular expression, so none has a match to limit. A date, a time, a
duration, a UUID, an IPv4 address, a URI, an IRI, a URI template, a JSON pointer and the local part of an e-mail
address are read in one pass, or in a fixed number of passes. The checks that do more than read have a limit of
length that no valid string passes, and a longer string is answered from its length: an IPv6 address is at most 64
bytes, a host name (and the host of an e-mail address) at most 253, and an internationalized host name (and the host
of an internationalized e-mail address) at most 1,012, which is 253 characters of four bytes. The regex format
reads a string of at most 1,048,576 bytes with groups nested at most 200 deep, and a string over one of those is not
a valid regular expression to it. It puts the group names of the string in order to find a name that is written
twice, which takes time in proportion to n log n for n names. So the time of every check is in proportion to the
length of the string, or to that and its logarithm. On a probe of 260 hostile strings of each length (long runs of
what each format repeats, and near misses at their ends), the slowest check of a string of 100,000 characters took
9 ms and of 1,000,000 characters 121 ms, which includes parsing the string. Before this, an internationalized
host name of 1,000,000 characters took more than a minute, and so did a regular expression of 125,000 groups of one
name.
On the thirteen files of the
ECMA-262 regular expression test data, 98,839 patterns
and 4.2 million answers, no pattern is over a limit of size, and with the limit at one second two patterns reach it
with Julia 1.13 and four with Julia 1.10, on one or a few strings each. All are patterns of nested repetitions of
what can match the empty string, such as ^a{2}?(((?!)|((|a){2,}(|a)\B|){2,3}?){2,3}.{3}a{2})\4. V8 refuses an
empty iteration and answers them at once, where PCRE2 tries each, which is more steps than the first stage of a
match has (157 million for one of them) and more than the code that reads the clock runs in a second. They answer
as V8 does with pattern_match_limit=nothing. Every other answer is V8's, but for the two lookbehind uses and the
case-insensitive backreference above, which are refused.
Surrogates
A JSON string with the escape of a surrogate that is not half of a pair, such as "\ud800", is not Unicode text. The
parser refuses a document that has one, an instance or a schema, so a pattern is never matched against such a string.
The long-term support release
Julia 1.10 bundles PCRE2 10.42, whose case folding is that of Unicode 14. A case-insensitive backreference there
does not hold equivalent the two characters of each of the thirty pairs below, which Unicode 15.1 and 16 made
equivalent and which V8 and Julia 1.13 hold equivalent. So ^(.)(?i:\1)$ matches ƛ with Julia 1.13 and
does not with Julia 1.10. Nothing else differs between the two: a literal, a class and every other construct read the
package's own tables on every Julia. The package does not work around this. The fix is to update to Julia 1.13, where
the answers are exact.
These thirty are every pair: the package's tests try each character that has a case variant against each other. Of the thirteen files of the ECMA-262 regular expression test data, Julia 1.10 differs from V8 on 1,026 answers, in twenty patterns of the case-folding file, and each of them is a text of two characters that are one of these pairs.
| Code points | Characters | Code points | Characters |
|---|---|---|---|
| U+019B, U+A7DC | ƛ | U+0264, U+A7CB | ɤ |
| U+0390, U+1FD3 | ΐ ΐ | U+03B0, U+1FE3 | ΰ ΰ |
| U+1C89, U+1C8A | | U+A7CC, U+A7CD | |
| U+A7DA, U+A7DB | | U+FB05, U+FB06 | ſt st |
| U+10D50, U+10D70 | | U+10D51, U+10D71 | |
| U+10D52, U+10D72 | | U+10D53, U+10D73 | |
| U+10D54, U+10D74 | | U+10D55, U+10D75 | |
| U+10D56, U+10D76 | | U+10D57, U+10D77 | |
| U+10D58, U+10D78 | | U+10D59, U+10D79 | |
| U+10D5A, U+10D7A | | U+10D5B, U+10D7B | |
| U+10D5C, U+10D7C | | U+10D5D, U+10D7D | |
| U+10D5E, U+10D7E | | U+10D5F, U+10D7F | |
| U+10D60, U+10D80 | | U+10D61, U+10D81 | |
| U+10D62, U+10D82 | | U+10D63, U+10D83 | |
| U+10D64, U+10D84 | | U+10D65, U+10D85 | |
The package's own tables hold Unicode 17, which added 28 pairs more: U+A7CE and U+A7CF, U+A7D2 and U+A7D3, U+A7D4 and U+A7D5, and U+16EA0 to U+16EB8 each with the character 27 places on (U+16EBB to U+16ED3). A case-insensitive literal or class holds them equivalent on every Julia. A case-insensitive backreference does not, with Julia 1.10 or 1.13, and neither does V8 13.6, whose data is Unicode 16.
Performance
Measured with jsonschema-benchmark's 37 corpora, each implementation in its own container pinned to the same 8 CPUs, the median of 3 runs. The Julia, Go, Java and .NET JIT harnesses warm up for 2 seconds (at least 100 passes) and report the last warm-up pass. The others are the benchmark's own harnesses. Each figure is the geometric mean of Julia's time over the other's (below 1 means Julia is faster), with how many corpora Julia is faster on.
| Julia 1.13 over | Warm validation | Cold validation | Compile | Parse |
|---|---|---|---|---|
| Blaze | 0.80 (28 of 37) | 0.83 (26 of 37) | 0.27 (37 of 37) | 0.43 (37 of 37) |
| Corvus Go | 1.15 (2 of 37) | 1.33 (3 of 37) | 1.80 (0 of 37) | 1.21 (4 of 37) |
| Corvus Rust | 1.47 (3 of 37) | 1.58 (0 of 37) | 1.17 (8 of 37) | 1.19 (2 of 37) |
| Corvus .NET, native AOT, interpreting the schema | 1.29 (2 of 37) | 1.36 (2 of 37) | 1.27 (5 of 37) | 1.04 (13 of 37) |
| Corvus .NET, the JIT with runtime code generation | 2.45 (0 of 37) | 0.006 (37 of 37) | 0.030 (37 of 37) | 0.20 (37 of 37) |
| Corvus Java | 2.17 (2 of 37) | 0.023 (37 of 37) | 0.015 (37 of 37) | 0.23 (37 of 37) |
Against JSONSchema.jl, which jsonschema-benchmark does not have, the two were run in one process with their timed passes interleaved, on the corpora whose drafts JSONSchema.jl implements (4, 6 and 7). A warm pass with CorvusJsonSchema takes 0.4% to 4.2% of JSONSchema.jl's time on 34 of them, 1.6% at the geometric mean. On the thirty-fifth, ui5, JSONSchema.jl takes about seven minutes a pass.
A schema is interpreted from compiled plans, as in the Rust crate and the Go module, and not turned into generated code, as it is in Java and in .NET on the JIT. Julia compiles each generated function through LLVM before its first run, which made compiling a schema take tens of seconds for a large one. The interpreter is compiled into the package image instead, so a process that loads the package compiles nothing to validate.
Links
- Package: the General registry
- Source and README: src-jl/CorvusJsonSchema
- The other languages: see Other languages