Skip to content
Closed
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
18 changes: 17 additions & 1 deletion dotnet/src/JsonStructure/Validation/SchemaValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ public sealed class SchemaValidator
// Alternate names
"altnames",
// Units
"unit"
"unit", "ucumUnit",
// Relations
"identity", "relations", "targettype", "cardinality", "scope", "qualifiertype"
};

private static readonly Regex NamespacePattern = new(
Expand Down Expand Up @@ -1131,6 +1133,10 @@ private void ValidateCrossTypeConstraints(JsonObject schema, string? typeStr, st
AddError(result, ErrorCodes.SchemaConstraintInvalidForType, $"'exclusiveMinimum' constraint is only valid for numeric types, not '{typeStr}'", AppendPath(path, "exclusiveMinimum"));
if (schema.ContainsKey("exclusiveMaximum"))
AddError(result, ErrorCodes.SchemaConstraintInvalidForType, $"'exclusiveMaximum' constraint is only valid for numeric types, not '{typeStr}'", AppendPath(path, "exclusiveMaximum"));
if (schema.ContainsKey("unit"))
AddError(result, ErrorCodes.SchemaConstraintInvalidForType, $"'unit' keyword is only valid for numeric types, not '{typeStr}'", AppendPath(path, "unit"));
if (schema.ContainsKey("ucumUnit"))
AddError(result, ErrorCodes.SchemaConstraintInvalidForType, $"'ucumUnit' keyword is only valid for numeric types, not '{typeStr}'", AppendPath(path, "ucumUnit"));
}

// Array constraints on non-array types
Expand Down Expand Up @@ -1568,6 +1574,16 @@ private void ValidateNumericSchema(JsonObject schema, string path, ValidationRes
ValidatePositiveNumber(schema, "multipleOf", path, result);
AddExtensionKeywordWarning(result, "multipleOf", path);
}

if (schema.TryGetPropertyValue("unit", out var unitValue))
{
ValidateStringProperty(unitValue, "unit", path, result);
}

if (schema.TryGetPropertyValue("ucumUnit", out var ucumUnitValue))
{
ValidateStringProperty(ucumUnitValue, "ucumUnit", path, result);
}
}

private void ValidateEnum(JsonNode? value, string path, ValidationResult result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ public final class SchemaValidator {
// Alternate names
"altnames",
// Units
"unit"
"unit", "ucumUnit",
// Relations
"identity", "relations", "targettype", "cardinality", "scope", "qualifiertype"
);

private static final Pattern NAMESPACE_PATTERN = Pattern.compile(
Expand Down Expand Up @@ -637,6 +639,14 @@ private void validateTypeConstraintCompatibility(ObjectNode schema, String typeS
addError(result, ErrorCodes.SCHEMA_CONSTRAINT_INVALID_FOR_TYPE, "'multipleOf' constraint is only valid for numeric types, not '" + typeStr + "'",
appendPath(path, "multipleOf"));
}
if (schema.has("unit")) {
addError(result, ErrorCodes.SCHEMA_CONSTRAINT_INVALID_FOR_TYPE, "'unit' keyword is only valid for numeric types, not '" + typeStr + "'",
appendPath(path, "unit"));
}
if (schema.has("ucumUnit")) {
addError(result, ErrorCodes.SCHEMA_CONSTRAINT_INVALID_FOR_TYPE, "'ucumUnit' keyword is only valid for numeric types, not '" + typeStr + "'",
appendPath(path, "ucumUnit"));
}
}

// String constraints on non-string types
Expand Down Expand Up @@ -1222,6 +1232,14 @@ private void validateNumericSchema(ObjectNode schema, String path, ValidationRes
validateNumber(schema, "exclusiveMinimum", path, result);
validateNumber(schema, "exclusiveMaximum", path, result);
validatePositiveNumber(schema, "multipleOf", path, result);

if (schema.has("unit")) {
validateStringProperty(schema.get("unit"), "unit", path, result);
}

if (schema.has("ucumUnit")) {
validateStringProperty(schema.get("ucumUnit"), "ucumUnit", path, result);
}

// Check minimum <= maximum
if (schema.has("minimum") && schema.has("maximum")) {
Expand Down
28 changes: 26 additions & 2 deletions perl/lib/JSON/Structure/SchemaValidator.pm
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ my %RESERVED_KEYWORDS = map { $_ => 1 } qw(
$offers abstract additionalProperties const default
description enum examples format items maxLength
name precision properties required scale type
values choices selector tuple
values choices selector tuple unit ucumUnit
identity relations targettype cardinality scope qualifiertype
);

# Extended keywords for conditional composition
Expand Down Expand Up @@ -112,7 +113,7 @@ my %VALID_FORMATS = map { $_ => 1 } qw(
# Known extensions
my %KNOWN_EXTENSIONS = map { $_ => 1 } qw(
JSONStructureImport JSONStructureAlternateNames JSONStructureUnits
JSONStructureConditionalComposition JSONStructureValidation
JSONStructureRelations JSONStructureConditionalComposition JSONStructureValidation
);

sub new {
Expand Down Expand Up @@ -1384,6 +1385,16 @@ sub _check_constraint_type_mismatch {
}
}

for my $keyword (qw(unit ucumUnit)) {
if ( exists $schema->{$keyword} && !$is_numeric ) {
$self->_add_error(
SCHEMA_CONSTRAINT_TYPE_MISMATCH,
"Keyword '$keyword' is only valid for numeric types, not '$type'",
"$path/$keyword"
);
}
}

# String constraints can only be on string types
my @string_types =
qw(string date time datetime duration uri base64 binary uuid jsonpointer name);
Expand Down Expand Up @@ -1427,6 +1438,19 @@ sub _validate_extended_keywords {
}
}

for my $keyword (qw(unit ucumUnit)) {
if ( exists $schema->{$keyword} ) {
my $value = $schema->{$keyword};
if ( !defined $value || ref($value) ) {
$self->_add_error(
SCHEMA_KEYWORD_INVALID_TYPE,
"$keyword must be a string",
"$path/$keyword"
);
}
}
}

# Check min/max relationships
if ( exists $schema->{minimum} && exists $schema->{maximum} ) {
if ( $schema->{minimum} > $schema->{maximum} ) {
Expand Down
1 change: 1 addition & 0 deletions php/src/JsonStructure/Types.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ final class Types
'JSONStructureImport',
'JSONStructureAlternateNames',
'JSONStructureUnits',
'JSONStructureRelations',
'JSONStructureConditionalComposition',
'JSONStructureValidation',
];
Expand Down
7 changes: 4 additions & 3 deletions python/src/json_structure/instance_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

Additionally, if the instance provides a "$uses" clause containing "JSONStructureConditionalComposition" and/or "JSONStructureValidation",
the corresponding conditional composition and validation addin constraints are enforced.
Extensions such as "JSONStructureAlternateNames" or "JSONStructureUnits" are generally ignored for validation.
Extensions such as "JSONStructureAlternateNames", "JSONStructureUnits", or "JSONStructureRelations" are generally ignored for validation.

Furthermore, when the root schema’s "$schema" equals
"https://json-structure.org/meta/extended/v0/#"
Expand Down Expand Up @@ -135,7 +135,8 @@ def validate_instance(self, instance, schema=None, path="#", meta=None):
"JSONStructureConditionalComposition",
"JSONStructureValidation",
"JSONStructureUnits",
"JSONStructureAlternateNames"
"JSONStructureAlternateNames",
"JSONStructureRelations"
]
schema.setdefault("$uses", [])
for addin in all_addins:
Expand Down Expand Up @@ -1227,7 +1228,7 @@ def _apply_uses(self, schema, instance):
offers = self.root_schema.get("$offers", {})
merged = dict(schema)
merged.setdefault("properties", {})
for use in [u for u in uses if not u in ["JSONStructureValidation", "JSONStructureConditionalComposition", "JSONStructureAlternateNames", "JSONStructureUnits"]]:
for use in [u for u in uses if not u in ["JSONStructureValidation", "JSONStructureConditionalComposition", "JSONStructureAlternateNames", "JSONStructureUnits", "JSONStructureRelations"]]:
if use not in offers:
self.errors.append(f"Add-in '{use}' not offered in $offers")
continue
Expand Down
19 changes: 15 additions & 4 deletions python/src/json_structure/schema_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ class JSONStructureSchemaCoreValidator:
"$offers", "abstract", "additionalProperties", "const", "default",
"description", "enum", "examples", "format", "items", "maxLength",
"name", "precision", "properties", "required", "scale", "type",
"values", "choices", "selector", "tuple"
"values", "choices", "selector", "tuple", "unit", "ucumUnit",
"identity", "relations", "targettype", "cardinality", "scope", "qualifiertype"
}
PRIMITIVE_TYPES = {
"string", "number", "integer", "boolean", "null", "int8", "uint8", "int16", "uint16",
Expand Down Expand Up @@ -77,7 +78,7 @@ class JSONStructureSchemaCoreValidator:
# Extension names
KNOWN_EXTENSIONS = {
"JSONStructureImport", "JSONStructureAlternateNames", "JSONStructureUnits",
"JSONStructureConditionalComposition", "JSONStructureValidation"
"JSONStructureRelations", "JSONStructureConditionalComposition", "JSONStructureValidation"
}

def __init__(self, allow_dollar=False, allow_import=False, import_map=None, extended=False, external_schemas=None, warn_on_unused_extension_keywords=True, max_validation_depth=64):
Expand Down Expand Up @@ -706,19 +707,23 @@ def _check_extended_validation_keywords(self, obj, path):
# Check for constraint type mismatches
string_constraints = ["minLength", "maxLength", "pattern"]
numeric_constraints = ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum", "multipleOf"]
numeric_annotation_keywords = ["unit", "ucumUnit"]
array_constraints = ["minItems", "maxItems", "uniqueItems", "contains", "minContains", "maxContains"]

# Check string constraints on non-string types
if tval != "string":
for key in string_constraints:
if key in obj:
self._err(f"'{key}' constraint is only valid for string type, not '{tval}'.", f"{path}/{key}")

# Check numeric constraints on non-numeric types
if tval not in numeric_types:
for key in numeric_constraints:
if key in obj:
self._err(f"'{key}' constraint is only valid for numeric types, not '{tval}'.", f"{path}/{key}")
for key in numeric_annotation_keywords:
if key in obj:
self._err(f"'{key}' keyword is only valid for numeric types, not '{tval}'.", f"{path}/{key}")

# Check array constraints on non-array types
if tval not in array_types:
Expand All @@ -729,6 +734,7 @@ def _check_extended_validation_keywords(self, obj, path):
# Now validate the constraint values for matching types
if tval in numeric_types:
self._check_numeric_validation(obj, path, tval, validation_enabled)
self._check_units_keywords(obj, path)
elif tval == "string":
self._check_string_validation(obj, path, validation_enabled)
elif tval in ["array", "set"]:
Expand Down Expand Up @@ -771,6 +777,11 @@ def _check_numeric_validation(self, obj, path, type_name, validation_enabled=Tru
if min_val > max_val:
self._err("'minimum' cannot be greater than 'maximum'.", f"{path}")

def _check_units_keywords(self, obj, path):
for key in ["unit", "ucumUnit"]:
if key in obj and not isinstance(obj[key], str):
self._err(f"'{key}' must be a string.", f"{path}/{key}")

def _check_string_validation(self, obj, path, validation_enabled=True):
"""
Check string validation keywords.
Expand Down
5 changes: 4 additions & 1 deletion rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,9 @@ pub const SCHEMA_KEYWORDS: &[&str] = &[
// Alternate names
"altnames",
// Units
"unit",
"unit", "ucumUnit",
// Relations
"identity", "relations", "targettype", "cardinality", "scope", "qualifiertype",
];

/// Validation extension keywords that require JSONStructureValidation.
Expand Down Expand Up @@ -374,6 +376,7 @@ pub const KNOWN_EXTENSIONS: &[&str] = &[
"JSONStructureImport",
"JSONStructureAlternateNames",
"JSONStructureUnits",
"JSONStructureRelations",
"JSONStructureConditionalComposition",
"JSONStructureValidation",
];
Expand Down
Loading