validateFunctionSchema function
void
validateFunctionSchema(})
Validate decoded project values or generated private schemas without coercion.
references is generated from TypeBox; callers cannot supply remote schemas.
Implementation
void validateFunctionSchema(
Map<String, Object?> schema,
Object? value, {
Map<String, Object?> references = const {},
int maxDepth = 8,
bool utf16StringBounds = false,
}) {
var nodes = 0;
void visit(Map<String, Object?> node, Object? value, int depth) {
Never invalid() =>
throw const FormatException('Value violates its function schema.');
if (++nodes > 100000 || depth > maxDepth) invalid();
if (node[r'$ref'] case final String ref) {
final target = references[ref];
if (target is! Map<String, Object?>) {
throw StateError('Missing generated schema.');
}
visit(target, value, depth);
return;
}
if (node['anyOf'] case final List choices) {
for (final choice in choices) {
try {
visit((choice as Map).cast<String, Object?>(), value, depth);
return;
} on FormatException {
continue;
}
}
invalid();
}
// A nullable type list accepts null before any enum/const check, matching
// the TypeScript validator and the generated nullable enum models.
final declared = node['type'];
if (value == null && declared is List && declared.contains('null')) return;
if (node.containsKey('const') && value != node['const'] ||
node['enum'] is List && !(node['enum'] as List).contains(value)) {
invalid();
}
final type = declared is List ? declared.last : declared;
switch (type) {
case 'null':
if (value != null) invalid();
case 'boolean':
if (value is! bool) invalid();
case 'number':
case 'integer':
if (value is! num ||
!value.isFinite ||
type == 'integer' &&
(value.abs() > 9007199254740991 ||
value != value.truncateToDouble()) ||
node['minimum'] is num && value < (node['minimum'] as num) ||
node['maximum'] is num && value > (node['maximum'] as num)) {
invalid();
}
case 'string':
if (value is! String || !_wellFormed(value)) invalid();
final length = utf16StringBounds ? value.length : value.runes.length;
if (node['minLength'] is num && length < (node['minLength'] as num) ||
node['maxLength'] is num && length > (node['maxLength'] as num) ||
node['pattern'] is String &&
!RegExp(node['pattern'] as String).hasMatch(value)) {
invalid();
}
case 'array':
if (value is! List ||
node['minItems'] is num &&
value.length < (node['minItems'] as num) ||
node['maxItems'] is num &&
value.length > (node['maxItems'] as num)) {
invalid();
}
if (node['uniqueItems'] == true &&
value.map(jsonEncode).toSet().length != value.length) {
invalid();
}
final items = node['items'];
for (var i = 0; i < value.length; i++) {
if (items is List && i >= items.length) {
if (node['additionalItems'] == false) invalid();
continue;
}
visit(
((items is List ? items[i] : items) as Map).cast<String, Object?>(),
value[i],
depth + 1,
);
}
case 'object':
if (value is! Map ||
value.keys.any((key) => key is! String) ||
node['minProperties'] is num &&
value.length < (node['minProperties'] as num) ||
node['maxProperties'] is num &&
value.length > (node['maxProperties'] as num)) {
invalid();
}
if ((node['required'] as List? ?? []).any(
(key) => !value.containsKey(key),
)) {
invalid();
}
final properties = (node['properties'] as Map? ?? {})
.cast<String, Object?>();
final patterns = (node['patternProperties'] as Map? ?? {})
.cast<String, Object?>();
for (final entry in value.entries) {
Object? child = properties[entry.key];
for (final pattern in patterns.entries) {
if (RegExp(pattern.key).hasMatch(entry.key)) child = pattern.value;
}
child ??= node['additionalProperties'] is Map
? node['additionalProperties']
: null;
if (child == null && node['additionalProperties'] == false) invalid();
if (child != null) {
visit(
(child as Map).cast<String, Object?>(),
entry.value,
depth + 1,
);
}
}
}
}
visit(schema, value, 1);
}