Create minimal JSON schemas from custom Julia types.
Current restrictions:
- no parametric types
- no Union types, except
Union{Nothing, T}
for optional fields - no abstract types in fields, only concrete types
- must define
StructTypes.StructType
for your custom types if you want touse_references=true
- must define
StructTypes.omitempties
for optional fields
using JSONSchemaGenerator, StructTypes
struct OptionalFieldSchema
int::Int
optional::Union{Nothing, String}
end
StructTypes.StructType(::Type{OptionalFieldSchema}) = StructTypes.Struct()
StructTypes.omitempties(::Type{OptionalFieldSchema}) = (:optional,)
struct NestedFieldSchema
int::Int
field::OptionalFieldSchema
vector::Vector{OptionalFieldSchema}
end
StructTypes.StructType(::Type{NestedFieldSchema}) = StructTypes.Struct()
schema_dict = JSONSchemaGenerator.schema(NestedFieldSchema)
You can easily print the schema with JSON or JSON3
julia> using JSON
julia> JSON.print(schema_dict, 2)
{
"type": "object",
"properties": {
"int": {
"type": "integer"
},
"field": {
"type": "object",
"properties": {
"int": {
"type": "integer"
},
"optional": {
"type": "string"
}
},
"required": [
"int"
]
},
"vector": {
"type": "array",
"items": {
"type": "object",
"properties": {
"int": {
"type": "integer"
},
"optional": {
"type": "string"
}
},
"required": [
"int"
]
}
}
},
"required": [
"int",
"field",
"vector"
]
}
By default the generated schema is recursively nested, meaning that any repeating type will be generated multiple times. The use_references=true
keyword argument can generate the JSON references for you. You can see that the OptionalFieldSchema is now referenced with $ref
towards the $defs
section of the schema instead of being copied.
julia> schema_dict = JSONSchemaGenerator.schema(NestedFieldSchema, use_references=true);
julia> JSON.print(schema_dict, 2)
{
"type": "object",
"properties": {
"int": {
"type": "integer"
},
"field": {
"$ref": "#/$defs/OptionalFieldSchema"
},
"vector": {
"type": "array",
"items": {
"$ref": "#/$defs/OptionalFieldSchema"
}
}
},
"required": [
"int",
"field",
"vector"
],
"$defs": {
"OptionalFieldSchema": {
"type": "object",
"properties": {
"int": {
"type": "integer"
},
"optional": {
"type": "string"
}
},
"required": [
"int"
]
}
}
}
JSONSchema.jl provides validation for JSON schemas. The schema dictionary generated by JSONSchemaGenerator.jl works together with JSONSchema.jl.
JSONSchema.jl works with JSON.jl parsing, but JSON3.jl is better for direct JSON (de)serialization with StructTypes.jl definitions, so unfortunately you may need to use both JSON.jl and JSON3.jl, especially if you have optional fields defined with Union{Nothing, T}
.
Let's use the example above to generate a JSON string and validate it:
using JSONSchema, JSON3, JSON
schema_dict = JSONSchemaGenerator.schema(NestedFieldSchema, use_references=true)
obj = NestedFieldSchema(
1,
OptionalFieldSchema(2, nothing),
[OptionalFieldSchema(2, "string"), OptionalFieldSchema(2, nothing)]
)
# parsing back into a Dict, because that is what JSONSchema.validate wants
json_dict = JSON3.write(obj) |> JSON.parse
JSONSchema.validate(JSONSchema.Schema(schema_dict), json_dict) === nothing
JSONSchemaGenerator.jl provides a function combinationkeywords(::Type)
which can be used to associate a struct with an array of special types AllOf{T,S}
, AnyOf{T,S}
, OneOf{T,S}
and Not{T}
that allow the corresponding JSON keyword to be generated in a schema (see Boolean JSON Schema combination). Note that more than two schemas can be combined by chaining: e.g. AllOf{A, AllOf{B, C}}
.
In the following example we combine some schemas that check if fields are equal to specific values (using Val
and Tuple
types, noting that these do not serialize well and should only be used for validation purposes like this):
import JSONSchemaGenerator as JSG
using JSONSchema, JSON3
struct ConstantInt1Schema
int::Val{1}
end
struct EnumInt2Or3Schema
int::Tuple{2,3}
end
struct ConstantBoolTrueSchema
bool::Val{true}
end
struct BooleanCombinationSchema
int::Int
bool::Bool
end
StructTypes.StructType(::Type{BooleanCombinationSchema}) = StructTypes.Struct()
JSG.combinationkeywords(::Type{BooleanCombinationSchema}) = [
JSG.AllOf{
JSG.AnyOf{ConstantInt1Schema, EnumInt2Or3Schema},
JSG.Not{ConstantBoolTrueSchema}
}
]
schema_dict = JSG.schema(BooleanCombinationSchema)
good_json = JSON3.write(BooleanCombinationSchema(2, false))
bad_json = JSON3.write(BooleanCombinationSchema(5, true))
JSONSchema.validate(JSONSchema.Schema(schema_dict), good_json) === nothing
JSONSchema.validate(JSONSchema.Schema(schema_dict), bad_json) !== nothing
The printed schema looks as follows:
julia> JSON.print(schema_dict, 2)
JSON.print(schema_dict, 2)
{
"type": "object",
"properties": {
"int": {
"type": "integer"
},
"bool": {
"type": "boolean"
}
},
"required": [
"int",
"bool"
],
"allOf": [
{
"anyOf": [
{
"type": "object",
"properties": {
"int": {
"const": 1
}
},
"required": [
"int"
]
},
{
"type": "object",
"properties": {
"int": {
"enum": [
2,
3
]
}
},
"required": [
"int"
]
}
]
},
{
"not": {
"type": "object",
"properties": {
"bool": {
"const": true
}
},
"required": [
"bool"
]
}
}
]
}