Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 82 additions & 40 deletions scripts/json_schema_validator.gd
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const ERR_RANGE_F = "Key %s that equal %f must be %s than %f"
const ERR_RANGE_S = "Length of '%s' (%d) %s than declared (%d)"
const ERR_WRONG_PATTERN = "String '%s' does not match its corresponding pattern"
const ERR_FORMAT = "String '%s' does not match its corresponding format '%s'"
const ERR_ENUM = "Value %s is not one of the allowed enum values for '%s'"

# This is one and only function that need you to call outside
# If all validation checks passes, this return empty String
Expand All @@ -109,7 +110,11 @@ func validate(json_data : String, schema: String) -> String:
TYPE_DICTIONARY:
if parsed_schema.empty():
return ""
elif parsed_schema.keys().size() > 0 && !parsed_schema.has(JSKW_TYPE):
elif (
parsed_schema.keys().size() > 0 and
not parsed_schema.has(JSKW_TYPE) and
not parsed_schema.has(JSKW_ENUM)
):
return ERR_WRONG_SCHEMA_TYPE
_: return ERR_WRONG_SCHEMA_TYPE

Expand All @@ -136,46 +141,53 @@ func _type_selection(json_data: String, schema: Dictionary, key: String = DEF_KE
else:
return ERR_INVALID_JSON_GEN + "false is always invalid"

var typearr: Array = _var_to_array(schema.type)
var parsed_data = parse_json(json_data)
var error: String = ERR_TYPE_MISMATCH_GEN % [typearr, key]
for type in typearr:
match type:
JST_ARRAY:
if typeof(parsed_data) == TYPE_ARRAY:
error = _validate_array(parsed_data, schema, key)
else:
error = ERR_TYPE_MISMATCH_GEN % [[JST_ARRAY], key]
JST_BOOLEAN:
if typeof(parsed_data) != TYPE_BOOL:
return ERR_TYPE_MISMATCH_GEN % [[JST_BOOLEAN], key]
else:
error = ""
JST_INTEGER:
if typeof(parsed_data) == TYPE_INT:
error = _validate_integer(parsed_data, schema, key)
if typeof(parsed_data) == TYPE_REAL && parsed_data == int(parsed_data):
error = _validate_integer(int(parsed_data), schema, key)
JST_NULL:
if typeof(parsed_data) != TYPE_NIL:
return ERR_TYPE_MISMATCH_GEN % [[JST_NULL], key]
else:
error = ""
JST_NUMBER:
if typeof(parsed_data) == TYPE_REAL:
error = _validate_number(parsed_data, schema, key)
else:
error = ERR_TYPE_MISMATCH_GEN % [[JST_NUMBER], key]
JST_OBJECT:
if typeof(parsed_data) == TYPE_DICTIONARY:
error = _validate_object(parsed_data, schema, key)
else:
error = ERR_TYPE_MISMATCH_GEN % [[JST_OBJECT], key]
JST_STRING:
if typeof(parsed_data) == TYPE_STRING:
error = _validate_string(parsed_data, schema, key)
else:
error = ERR_TYPE_MISMATCH_GEN % [[JST_STRING], key]
var error: String

if schema.has(JSKW_TYPE):
var typearr: Array = _var_to_array(schema.type)
error = ERR_TYPE_MISMATCH_GEN % [typearr, key]
for type in typearr:
match type:
JST_ARRAY:
if typeof(parsed_data) == TYPE_ARRAY:
error = _validate_array(parsed_data, schema, key)
else:
error = ERR_TYPE_MISMATCH_GEN % [[JST_ARRAY], key]
JST_BOOLEAN:
if typeof(parsed_data) != TYPE_BOOL:
return ERR_TYPE_MISMATCH_GEN % [[JST_BOOLEAN], key]
else:
error = ""
JST_INTEGER:
if typeof(parsed_data) == TYPE_INT:
error = _validate_integer(parsed_data, schema, key)
if typeof(parsed_data) == TYPE_REAL && parsed_data == int(parsed_data):
error = _validate_integer(int(parsed_data), schema, key)
JST_NULL:
if typeof(parsed_data) != TYPE_NIL:
return ERR_TYPE_MISMATCH_GEN % [[JST_NULL], key]
else:
error = ""
JST_NUMBER:
if typeof(parsed_data) == TYPE_REAL:
error = _validate_number(parsed_data, schema, key)
else:
error = ERR_TYPE_MISMATCH_GEN % [[JST_NUMBER], key]
JST_OBJECT:
if typeof(parsed_data) == TYPE_DICTIONARY:
error = _validate_object(parsed_data, schema, key)
else:
error = ERR_TYPE_MISMATCH_GEN % [[JST_OBJECT], key]
JST_STRING:
if typeof(parsed_data) == TYPE_STRING:
error = _validate_string(parsed_data, schema, key)
else:
error = ERR_TYPE_MISMATCH_GEN % [[JST_STRING], key]

if schema.has(JSKW_ENUM):
error = _validate_enum(parsed_data, schema, key)

return error


Expand Down Expand Up @@ -511,3 +523,33 @@ func _validate_string(input_data: String, input_schema: Dictionary, property_nam
return ERR_INVALID_JSON_GEN % ERR_FORMAT % [property_name, JSKW_COLOR]

return error


func _enum_match(value, enum_item) -> bool:
var value_type = typeof(value)
var enum_type = typeof(enum_item)

# Dicts are compared by reference by default - so we use the dicts hash value here.
# (Dictionaries are ordered since Godot 3.0)
# (If hash collisions are an actual issue we have to write a comparison util).
if value_type == TYPE_DICTIONARY and enum_type == TYPE_DICTIONARY:
return value.hash() == enum_item.hash()

if not value_type == enum_type:
return false

return value == enum_item


func _validate_enum(value, schema: Dictionary, property_name: String = DEF_KEY_NAME) -> String:
# enum must be an array
if typeof(schema[JSKW_ENUM]) != TYPE_ARRAY:
return ERR_WRONG_SCHEMA_GEN + ERR_TYPE_MISMATCH_GEN % [JST_ARRAY, property_name+"."+JSKW_ENUM]

for item in schema[JSKW_ENUM]:
if _enum_match(value, item):
return ""

# Not found in enum
var val_str = JSON.print(value)
return ERR_INVALID_JSON_GEN % (ERR_ENUM % [val_str, property_name])
115 changes: 115 additions & 0 deletions test/Unit/test_json_schema_validator.gd
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,118 @@ func test_validate(params = use_parameters(test_is_json_valid_params)) -> void:
result == expected_result,
"Expected %s but was %s instead for json_data \"%s\" with json_schema \"%s\" and error: %s " % [expected_result, result, json_data, json_schema, error]
)


var test_enum_params = [
["green", {"enum": ["red", "blue", "green"]}, true],
# number enum match
[2, {"type":"number","enum":[1,2,3]}, true],
# number enum no match
[4, {"type":"number","enum":[1,2,3]}, false],
# string enum match
["red", {"type":"string","enum":["red","green"]}, true],
# string enum no match
["blue", {"type":"string","enum":["red","green"]}, false],
# object enum match (deep equality)
[{"b":2}, {"type":"object","enum":[{"a":1},{"b":2}]}, true],
# object enum no match
[{"a":1}, {"type":"object","enum":[{"a":2}]}, false],
# array enum match
[[1,2], {"type":"array","enum":[[1,2],[3,4]]}, true],
# invalid enum schema (not an array) should produce schema error -> treated as failure
[1, {"enum":"notarray"}, false],
]


func test_enum_validate(params = use_parameters(test_enum_params)) -> void:
# prepare
var json_data = JSON.print(params[0])
var json_schema = JSON.print(params[1])
var expected_result = params[2]

# test
var json_schema_validator = JSONSchema.new()
var error = json_schema_validator.validate(json_data, json_schema)

var result = error == ""

# validate
assert_true(
result == expected_result,
"Expected %s but was %s instead for json_data \"%s\" with json_schema \"%s\" and error: %s " % [expected_result, result, json_data, json_schema, error]
)


var test_enum_edge_params = [
["2", {"type":"string", "enum":["small", "medium", "large"]}, false],
# empty enum array -> nothing matches
[1, {"enum":[]}, false],
# null in enum should match null
[null, {"enum":[null, "a"]}, true],
# boolean enum
[true, {"enum":[false, true]}, true],
# mixed-type enum (string vs number) - string "1" should match
["1", {"enum":[1, "1"]}, true],
# array no match
[[1,2,3], {"type":"array","enum":[[1,2]]}, false],
]


func test_enum_edge_validate(params = use_parameters(test_enum_edge_params)) -> void:
# prepare
var json_data = JSON.print(params[0])
var json_schema = JSON.print(params[1])
var expected_result = params[2]

# test
var json_schema_validator = JSONSchema.new()
var error = json_schema_validator.validate(json_data, json_schema)

var result = error == ""

# validate
assert_true(
result == expected_result,
"Expected %s but was %s instead for json_data \"%s\" with json_schema \"%s\" and error: %s " % [expected_result, result, json_data, json_schema, error]
)


var test_object_edge_params = [
# additionalProperties false with extra property -> fail
[{"a":"x","b":"y"}, {"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":false}, false],
# additionalProperties as schema -> extra prop validated by schema
[{"a":"x","b":2}, {"type":"object","properties":{"a":{"type":"string"}},"additionalProperties":{"type":"number"}}, true],
# required property missing -> fail
[{}, {"type":"object","properties":{"a":{"type":"string"}},"required":["a"]}, false],
# propertyNames pattern matching key names
[{"foo":"bar"}, {"type":"object","propertyNames":{"pattern":"^foo$"}}, true],
[{"bar":"baz"}, {"type":"object","propertyNames":{"pattern":"^foo$"}}, false],
# string minLength
["abc", {"type":"string","minLength":2}, true],
["a", {"type":"string","minLength":2}, false],
# multipleOf with float
[0.5, {"type":"number","multipleOf":0.25}, true],
[0.3, {"type":"number","multipleOf":0.25}, false],
# exclusiveMinimum
[5, {"type":"number","exclusiveMinimum":5}, false],
[6, {"type":"number","exclusiveMinimum":5}, true],
]


func test_object_edge_validate(params = use_parameters(test_object_edge_params)) -> void:
# prepare
var json_data = JSON.print(params[0])
var json_schema = JSON.print(params[1])
var expected_result = params[2]

# test
var json_schema_validator = JSONSchema.new()
var error = json_schema_validator.validate(json_data, json_schema)

var result = error == ""

# validate
assert_true(
result == expected_result,
"Expected %s but was %s instead for json_data \"%s\" with json_schema \"%s\" and error: %s " % [expected_result, result, json_data, json_schema, error]
)