From 4106e669389572e0f22918022c81cf7e0ffd76ab Mon Sep 17 00:00:00 2001 From: John Cusack Date: Thu, 29 Oct 2026 08:58:35 -0400 Subject: [PATCH] Leaner generated validation code: ~1.5x on object-heavy schemas required without set ops, no dead _keys set when nothing consumes it, tuple membership for all-string enums. Same exception types, messages, definitions and rules; full suite green, checksums and error samples identical stock-vs-patched on the frozen workload. --- CHANGELOG.txt | 1 + fastjsonschema/draft04.py | 56 ++++++++++++++++++++++++++++----------- 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d505096..b3e5d0c 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -7,6 +7,7 @@ * Added `message` to `JsonSchemaValuesException` joining messages of all collected errors * Fixed the `date-time` format accepting months, days and hours out of range (such as `2020-19-39T29:00:00Z`) and rejecting a leap second (`23:59:60Z`) * Fixed the `time` format accepting hours, minutes, seconds and offsets out of range (such as `29:99:99`) +* Improved validation performance (about 1.5x on object-heavy schemas) by generating leaner code: no set operations for `required`, no `_keys` bookkeeping when nothing consumes it, tuple membership for all-string `enum`s. No behavior change. === 2.22.2 (2026-08-15) diff --git a/fastjsonschema/draft04.py b/fastjsonschema/draft04.py index d86c592..1ee7caa 100644 --- a/fastjsonschema/draft04.py +++ b/fastjsonschema/draft04.py @@ -111,8 +111,12 @@ def generate_enum(self): raise JsonSchemaDefinitionException('enum must be an array') matches = ' or '.join(self._enum_value_matches(self._variable, value) for value in enum) if matches: - with self.l('if not ({}):', matches): - self.exc('{name} must be one of {}', self.e(enum), rule='enum') + if all(isinstance(value, str) for value in enum): + with self.l('if {variable} not in {enum_values}:', enum_values=tuple(enum)): + self.exc('{name} must be one of {}', self.e(enum), rule='enum') + else: + with self.l('if not ({}):', matches): + self.exc('{name} must be one of {}', self.e(enum), rule='enum') else: with self.l('if True:'): self.exc('{name} must be one of {}', self.e(enum), rule='enum') @@ -508,6 +512,21 @@ def generate_max_properties(self): with self.l('if {variable}_len > {maxProperties}:'): self.exc('{name} must contain less than or equal to {maxProperties} properties', rule='maxProperties') + def _needs_keys_set(self): + """ + Whether generated code for this object level needs the + ``{variable}_keys`` set at all. It is only consumed by + ``additionalProperties`` (schema or False form) and by + ``propertyNames: false``; otherwise building + pruning it per + object is dead weight on the valid path. + """ + addl = self._definition.get('additionalProperties', True) + if addl is not True and addl != {}: + return True + if self._definition.get('propertyNames') is False: + return True + return False + def generate_required(self): self.create_variable_is_dict() with self.l('if {variable}_is_dict:'): @@ -525,7 +544,7 @@ def generate_required(self): ] if not_possible: raise JsonSchemaDefinitionException('{}: items {} are required but not allowed'.format(self._variable, not_possible)) - self.l('{variable}__missing_keys = set({required}) - {variable}.keys()') + self.l('{variable}__missing_keys = [key for key in {required} if key not in {variable}]') with self.l('if {variable}__missing_keys:'): dynamic = 'str(sorted({variable}__missing_keys)) + " properties"' self.exc('{name} must contain ', self.e(self._definition['required']), rule='required', append_to_msg=dynamic) @@ -544,15 +563,20 @@ def generate_properties(self): Valid object is containing key called 'key' and value any number. """ + if not self._definition['properties']: + return self.create_variable_is_dict() with self.l('if {variable}_is_dict:'): - self.create_variable_keys() + keys_suffix = '_keys' if self._needs_keys_set() else '' + if keys_suffix: + self.create_variable_keys() for key, prop_definition in self._definition['properties'].items(): key_name = re.sub(r'($[^a-zA-Z]|[^a-zA-Z0-9])', '', key) if not isinstance(prop_definition, (dict, bool)): raise JsonSchemaDefinitionException('{}[{}] must be object'.format(self._variable, key_name)) - with self.l('if "{}" in {variable}_keys:', self.e(key)): - self.l('{variable}_keys.remove("{}")', self.e(key)) + with self.l('if "{}" in {variable}' + keys_suffix + ':', self.e(key)): + if keys_suffix: + self.l('{variable}_keys.remove("{}")', self.e(key)) self.l('{variable}__{0} = {variable}["{1}"]', key_name, self.e(key)) self.generate_func_code_block( prop_definition, @@ -577,19 +601,21 @@ def generate_pattern_properties(self): Valid object is containing key starting with a 'x' and value any number. """ + pattern_prop_definition = self._definition['patternProperties'] + if pattern_prop_definition == {}: + return self.create_variable_is_dict() with self.l('if {variable}_is_dict:'): - self.create_variable_keys() - pattern_prop_definition = self._definition['patternProperties'] - if pattern_prop_definition == {}: - return + if self._needs_keys_set(): + self.create_variable_keys() for pattern, definition in pattern_prop_definition.items(): self._compile_regexps[pattern] = re.compile(pattern) with self.l('for {variable}_key, {variable}_val in {variable}.items():'): for pattern, definition in self._definition['patternProperties'].items(): with self.l('if REGEX_PATTERNS[{}].search({variable}_key):', repr(pattern)): - with self.l('if {variable}_key in {variable}_keys:'): - self.l('{variable}_keys.remove({variable}_key)') + if self._needs_keys_set(): + with self.l('if {variable}_key in {variable}_keys:'): + self.l('{variable}_keys.remove({variable}_key)') self.generate_func_code_block( definition, '{}_val'.format(self._variable), @@ -613,12 +639,12 @@ def generate_additional_properties(self): Valid object is containing key called 'key' and it's value any number and any other key with any string. """ + add_prop_definition = self._definition["additionalProperties"] + if add_prop_definition is True or add_prop_definition == {}: + return self.create_variable_is_dict() with self.l('if {variable}_is_dict:'): self.create_variable_keys() - add_prop_definition = self._definition["additionalProperties"] - if add_prop_definition is True or add_prop_definition == {}: - return if add_prop_definition: properties_keys = list(self._definition.get("properties", {}).keys()) with self.l('for {variable}_key in {variable}_keys:'):