From a4b963eb6db65a3d742e0fe9e12c5066e2d65f5e Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:39:12 -0700 Subject: [PATCH] Generate non-finite float defaults as valid Python source A schema default of NaN or infinity was written into the generated validation function with repr(), which produces the bare names `nan`, `inf` and `-inf`. Those names do not exist in the generated module's global state, so compiling such a schema produced a function that raised `NameError: name 'nan' is not defined` as soon as the default was applied. Added `repr_default()`, which renders non-finite floats as `float('nan')` and recurses into lists, tuples and dicts so nested defaults are handled too; everything else still goes through plain repr(). It is used for both the default-assignment code and the `definition=` argument of the generated exceptions, which had the same leak. Fixes #101 --- fastjsonschema/draft04.py | 6 +++--- fastjsonschema/generator.py | 23 ++++++++++++++++++++++- tests/test_default.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/fastjsonschema/draft04.py b/fastjsonschema/draft04.py index b0db0be..5cecd64 100644 --- a/fastjsonschema/draft04.py +++ b/fastjsonschema/draft04.py @@ -2,7 +2,7 @@ import re from .exceptions import JsonSchemaDefinitionException -from .generator import CodeGenerator, enforce_list +from .generator import CodeGenerator, enforce_list, repr_default JSON_TYPE_TO_PYTHON_TYPE = { @@ -459,7 +459,7 @@ def generate_items(self): '{}[{}]'.format(self._variable_name, idx), ) if self._use_default and isinstance(item_definition, dict) and 'default' in item_definition: - self.l('else: {variable}.append({})', repr(item_definition['default'])) + self.l('else: {variable}.append({})', repr_default(item_definition['default'])) if 'additionalItems' in self._definition: if self._definition['additionalItems'] is False: @@ -558,7 +558,7 @@ def generate_properties(self): clear_variables=True, ) if self._use_default and isinstance(prop_definition, dict) and 'default' in prop_definition: - self.l('else: {variable}["{}"] = {}', self.e(key), repr(prop_definition['default'])) + self.l('else: {variable}["{}"] = {}', self.e(key), repr_default(prop_definition['default'])) def generate_pattern_properties(self): """ diff --git a/fastjsonschema/generator.py b/fastjsonschema/generator.py index 7858774..8a7efcd 100644 --- a/fastjsonschema/generator.py +++ b/fastjsonschema/generator.py @@ -292,7 +292,7 @@ def exc(self, msg, *args, append_to_msg=None, rule=None): ) definition = self._expand_refs(self._definition) definition_rule = self.e(definition.get(rule) if isinstance(definition, dict) else None) - self.l(msg, *args, definition=repr(definition), rule=repr(rule), definition_rule=definition_rule) + self.l(msg, *args, definition=repr_default(definition), rule=repr(rule), definition_rule=definition_rule) def _expand_refs(self, definition): if isinstance(definition, list): @@ -361,6 +361,27 @@ def serialize_regexes(patterns_dict): return '{\n ' + ",\n ".join(regex_patterns) + "\n}" +def repr_default(value): + """ + Like ``repr``, but renders non-finite floats as valid Python source. + + ``repr(float('nan'))`` is ``'nan'``, which is not a name available in the + generated code, so a schema default of NaN or infinity has to be written + out as a ``float(...)`` call instead. + """ + if isinstance(value, float) and (value != value or value in (float('inf'), float('-inf'))): + return "float({!r})".format(str(value)) + if isinstance(value, list): + return '[' + ', '.join(repr_default(item) for item in value) + ']' + if isinstance(value, tuple): + return '(' + ''.join(repr_default(item) + ', ' for item in value) + ')' + if isinstance(value, dict): + return '{' + ', '.join( + '{}: {}'.format(repr_default(k), repr_default(v)) for k, v in value.items() + ) + '}' + return repr(value) + + def repr_regex(regex): all_flags = ("A", "I", "DEBUG", "L", "M", "S", "X") flags = " | ".join(f"re.{f}" for f in all_flags if regex.flags & getattr(re, f)) diff --git a/tests/test_default.py b/tests/test_default.py index 93d69ff..6909dd4 100644 --- a/tests/test_default.py +++ b/tests/test_default.py @@ -1,3 +1,5 @@ +import math + import pytest from fastjsonschema import JsonSchemaValueException, validate @@ -46,3 +48,37 @@ def test_default_turned_off(): }, }, {}, use_default=False) assert output == {} + + +def test_default_non_finite_float(): + """Non-finite float defaults must be generated as valid Python source (#101).""" + output = validate({ + 'type': 'object', + 'properties': { + 'a': {'type': 'number', 'default': float('nan')}, + 'b': {'type': 'number', 'default': float('inf')}, + 'c': {'type': 'number', 'default': float('-inf')}, + }, + }, {}) + assert math.isnan(output['a']) + assert output['b'] == float('inf') + assert output['c'] == float('-inf') + + +def test_default_non_finite_float_in_array(): + output = validate({ + 'type': 'array', + 'items': [{'type': 'number', 'default': float('nan')}], + }, []) + assert len(output) == 1 and math.isnan(output[0]) + + +def test_default_nested_non_finite_float(): + output = validate({ + 'type': 'object', + 'properties': { + 'a': {'default': {'x': float('inf'), 'y': [1, float('nan')]}}, + }, + }, {}) + assert output['a']['x'] == float('inf') + assert math.isnan(output['a']['y'][1])