From bddce5c42b6384dc07fa77b53a51bfd01526b897 Mon Sep 17 00:00:00 2001 From: Wleeaf <179125857+wleeaf@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:11:25 +0000 Subject: [PATCH] Keep minimum and maximum inclusive in draft 06 and later Draft 06 and later reused the draft 04 minimum and maximum checks, which make the bound exclusive when exclusiveMinimum or exclusiveMaximum is truthy. Since draft 06 those keywords are numbers validated on their own, so {"maximum": 10, "exclusiveMaximum": 15} rejected 10. --- CHANGELOG.txt | 1 + fastjsonschema/draft06.py | 22 ++++++++++++++++++++++ tests/test_number.py | 15 +++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/CHANGELOG.txt b/CHANGELOG.txt index d505096..4cc89f5 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`) +* Fixed `minimum` and `maximum` being exclusive in draft 06 and later when `exclusiveMinimum` or `exclusiveMaximum` is set to a number other than zero === 2.22.2 (2026-08-15) diff --git a/fastjsonschema/draft06.py b/fastjsonschema/draft06.py index e744ef7..30eb517 100644 --- a/fastjsonschema/draft06.py +++ b/fastjsonschema/draft06.py @@ -83,6 +83,28 @@ def generate_type(self): with self.l('if not isinstance({variable}, ({})){}:', python_types, extra): self.exc('{name} must be {}', ' or '.join(types), rule='type') + def generate_minimum(self): + """ + Since draft 06 exclusiveMinimum is a number validated on its own, + so minimum is always inclusive. + """ + with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'): + if not isinstance(self._definition['minimum'], (int, float, decimal.Decimal)): + raise JsonSchemaDefinitionException('minimum must be a number') + with self.l('if {variable} < {minimum}:'): + self.exc('{name} must be bigger than or equal to {minimum}', rule='minimum') + + def generate_maximum(self): + """ + Since draft 06 exclusiveMaximum is a number validated on its own, + so maximum is always inclusive. + """ + with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'): + if not isinstance(self._definition['maximum'], (int, float, decimal.Decimal)): + raise JsonSchemaDefinitionException('maximum must be a number') + with self.l('if {variable} > {maximum}:'): + self.exc('{name} must be smaller than or equal to {maximum}', rule='maximum') + def generate_exclusive_minimum(self): with self.l('if isinstance({variable}, (int, float, Decimal)) and not isinstance({variable}, bool):'): if not isinstance(self._definition['exclusiveMinimum'], (int, float, decimal.Decimal)): diff --git a/tests/test_number.py b/tests/test_number.py index add0228..d278d26 100644 --- a/tests/test_number.py +++ b/tests/test_number.py @@ -177,6 +177,21 @@ def test_exclusive_numeric_keywords_ignore_boolean(asserter, schema_version, key }, value, value) +@pytest.mark.parametrize('schema_version', ( + 'http://json-schema.org/draft-06/schema', + 'http://json-schema.org/draft-07/schema', +)) +@pytest.mark.parametrize('keywords', ( + {'minimum': 10, 'exclusiveMinimum': 5}, + {'maximum': 10, 'exclusiveMaximum': 15}, +)) +def test_minimum_and_maximum_stay_inclusive_next_to_exclusive_keywords(asserter, schema_version, keywords): + asserter({ + '$schema': schema_version, + **keywords, + }, 10, 10) + + @pytest.mark.parametrize('value', ( 1.0, 0.1,