From dd8d9e7792b54a154e960a0beb0292d8db68f200 Mon Sep 17 00:00:00 2001 From: Mattias-Sehlstedt <60173714+Mattias-Sehlstedt@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:29:15 +0200 Subject: [PATCH] [python] centralize the Python constraint rules --- .../languages/AbstractPythonCodegen.java | 166 +++++++++++------- 1 file changed, 98 insertions(+), 68 deletions(-) diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java index f94a1dbf1bad..d741ba71d1e2 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractPythonCodegen.java @@ -1839,10 +1839,6 @@ public boolean isEmpty() { class PydanticType { - private static final String LESS_THAN = "lt"; - private static final String GREATER_THAN = "gt"; - private static final String GREATER_OR_EQUAL_TO = "ge"; - private static final String LESS_OR_EQUAL_TO = "le"; private static final String TYPING = "typing"; private static final String DECIMAL = "Decimal"; @@ -1872,12 +1868,7 @@ public PydanticType( private PythonType arrayType(IJsonSchemaValidationProperties cp) { PythonType pt = new PythonType(); - if (cp.getMaxItems() != null) { - pt.constrain("max_length", cp.getMaxItems()); - } - if (cp.getMinItems() != null) { - pt.constrain("min_length", cp.getMinItems()); - } + ConstraintApplier.applyConstraints(cp, pt, ConstraintType.ARRAY); if (cp.getUniqueItems()) { // A unique "array" is a set // TODO: pydantic v2: Pydantic suggest to convert this to a set, but this has some implications: @@ -1915,12 +1906,7 @@ private PythonType stringType(IJsonSchemaValidationProperties cp) { // e.g. constr(regex=r'/[a-z]/i', strict=True) pt.constrain("strict", true); - if (cp.getMaxLength() != null) { - pt.constrain("max_length", cp.getMaxLength()); - } - if (cp.getMinLength() != null) { - pt.constrain("min_length", cp.getMinLength()); - } + ConstraintApplier.applyConstraints(cp, pt, ConstraintType.STRING); if (cp.getPattern() != null) { moduleImports.add(PYDANTIC, "field_validator"); @@ -1952,28 +1938,8 @@ private PythonType numberType(IJsonSchemaValidationProperties cp) { PythonType floatt = new PythonType("float"); PythonType intt = new PythonType("int"); - // e.g. confloat(ge=10, le=100, strict=True) - if (cp.getMaximum() != null) { - if (cp.getExclusiveMaximum()) { - floatt.constrain(LESS_THAN, cp.getMaximum(), false); - intt.constrain(LESS_THAN, (int) Math.ceil(Double.valueOf(cp.getMaximum()))); // e.g. < 7.59 => < 8 - } else { - floatt.constrain(LESS_OR_EQUAL_TO, cp.getMaximum(), false); - intt.constrain(LESS_OR_EQUAL_TO, (int) Math.floor(Double.valueOf(cp.getMaximum()))); // e.g. <= 7.59 => <= 7 - } - } - if (cp.getMinimum() != null) { - if (cp.getExclusiveMinimum()) { - floatt.constrain(GREATER_THAN, cp.getMinimum(), false); - intt.constrain(GREATER_THAN, (int) Math.floor(Double.valueOf(cp.getMinimum()))); // e.g. > 7.59 => > 7 - } else { - floatt.constrain(GREATER_OR_EQUAL_TO, cp.getMinimum(), false); - intt.constrain(GREATER_OR_EQUAL_TO, (int) Math.ceil(Double.valueOf(cp.getMinimum()))); // e.g. >= 7.59 => >= 8 - } - } - if (cp.getMultipleOf() != null) { - floatt.constrain("multiple_of", cp.getMultipleOf()); - } + ConstraintApplier.applyConstraints(cp, floatt, ConstraintType.NUMBER); + ConstraintApplier.applyConstraints(cp, intt, ConstraintType.ROUNDED_NUMBER); if ("Union[StrictFloat, StrictInt]".equals(mapNumberTo)) { floatt.constrain("strict", true); @@ -2013,7 +1979,7 @@ private PythonType intType(IJsonSchemaValidationProperties cp) { PythonType pt = new PythonType("int"); // e.g. conint(ge=10, le=100, strict=True) pt.constrain("strict", true); - applyConstraints(pt, cp); + ConstraintApplier.applyConstraints(cp, pt, ConstraintType.NUMBER); return pt; } else { moduleImports.add(PYDANTIC, "StrictInt"); @@ -2029,14 +1995,8 @@ private PythonType binaryType(IJsonSchemaValidationProperties cp) { // e.g. conbytes(min_length=2, max_length=10) bytest.constrain("strict", true); strt.constrain("strict", true); - if (cp.getMaxLength() != null) { - bytest.constrain("max_length", cp.getMaxLength()); - strt.constrain("max_length", cp.getMaxLength()); - } - if (cp.getMinLength() != null) { - bytest.constrain("min_length", cp.getMinLength()); - strt.constrain("min_length", cp.getMinLength()); - } + ConstraintApplier.applyConstraints(cp, bytest, ConstraintType.BINARY); + ConstraintApplier.applyConstraints(cp, strt, ConstraintType.BINARY); if (cp.getPattern() != null) { moduleImports.add(PYDANTIC, "field_validator"); // use validator instead as regex doesn't support flags, e.g. IGNORECASE @@ -2097,7 +2057,7 @@ private PythonType decimalType(IJsonSchemaValidationProperties cp) { if (cp.getHasValidation()) { // e.g. condecimal(ge=10, le=100, strict=True) pt.constrain("strict", true); - applyConstraints(pt, cp); + ConstraintApplier.applyConstraints(cp, pt, ConstraintType.NUMBER); } return pt; @@ -2315,26 +2275,6 @@ private PythonType getType(CodegenParameter cp) { return result; } - private void applyConstraints(PythonType pythonType, IJsonSchemaValidationProperties cp) { - if (cp.getMaximum() != null) { - if (cp.getExclusiveMaximum()) { - pythonType.constrain(LESS_THAN, cp.getMaximum(), false); - } else { - pythonType.constrain(LESS_OR_EQUAL_TO, cp.getMaximum(), false); - } - } - if (cp.getMinimum() != null) { - if (cp.getExclusiveMinimum()) { - pythonType.constrain(GREATER_THAN, cp.getMinimum(), false); - } else { - pythonType.constrain(GREATER_OR_EQUAL_TO, cp.getMinimum(), false); - } - } - if (cp.getMultipleOf() != null) { - pythonType.constrain("multiple_of", cp.getMultipleOf()); - } - } - private String finalizeType(CodegenParameter cp, PythonType pt) { if (!cp.required || cp.isNullable) { moduleImports.add("typing", "Optional"); @@ -2351,4 +2291,94 @@ private String finalizeType(CodegenParameter cp, PythonType pt) { return pt.asTypeConstraintWithAnnotations(moduleImports); } } + + private enum ConstraintType { + ARRAY, BINARY, STRING, NUMBER, ROUNDED_NUMBER + } + + private static class ConstraintApplier { + + private static final String MAX_LENGTH = "max_length"; + private static final String MIN_LENGTH = "min_length"; + private static final String LESS_THAN = "lt"; + private static final String GREATER_THAN = "gt"; + private static final String GREATER_OR_EQUAL_TO = "ge"; + private static final String LESS_OR_EQUAL_TO = "le"; + private static final String MULTIPLE_OF = "multiple_of"; + + static void applyConstraints(IJsonSchemaValidationProperties cp, PythonType pythonType, ConstraintType type) { + if (cp == null || pythonType == null) { + return; + } + + switch (type) { + case ARRAY: + if (cp.getMaxItems() != null) { + pythonType.constrain(MAX_LENGTH, cp.getMaxItems()); + } + if (cp.getMinItems() != null) { + pythonType.constrain(MIN_LENGTH, cp.getMinItems()); + } + case BINARY: + case STRING: + if (cp.getMaxLength() != null) { + pythonType.constrain(MAX_LENGTH, cp.getMaxLength()); + } + if (cp.getMinLength() != null) { + pythonType.constrain(MIN_LENGTH, cp.getMinLength()); + } + break; + case NUMBER: + case ROUNDED_NUMBER: + applyNumericConstraints(cp, pythonType, type == ConstraintType.ROUNDED_NUMBER); + break; + } + } + + private static void applyNumericConstraints(IJsonSchemaValidationProperties cp, PythonType pythonType, boolean rounded) { + if (cp.getMaximum() != null) { + if (rounded) { + if (cp.getExclusiveMaximum()) { + pythonType.constrain(LESS_THAN, ceilValue(cp.getMaximum())); // e.g. < 7.59 => < 8 + } else { + pythonType.constrain(LESS_OR_EQUAL_TO, floorValue(cp.getMaximum())); // e.g. <= 7.59 => <= 7 + } + } else { + if (cp.getExclusiveMaximum()) { + pythonType.constrain(LESS_THAN, cp.getMaximum(), false); + } else { + pythonType.constrain(LESS_OR_EQUAL_TO, cp.getMaximum(), false); + } + } + } + + if (cp.getMinimum() != null) { + if (rounded) { + if (cp.getExclusiveMinimum()) { + pythonType.constrain(GREATER_THAN, floorValue(cp.getMinimum())); // e.g. > 7.59 => > 7 + } else { + pythonType.constrain(GREATER_OR_EQUAL_TO, ceilValue(cp.getMinimum())); // e.g. >= 7.59 => >= 8 + } + } else { + if (cp.getExclusiveMinimum()) { + pythonType.constrain(GREATER_THAN, cp.getMinimum(), false); + } else { + pythonType.constrain(GREATER_OR_EQUAL_TO, cp.getMinimum(), false); + } + } + } + + if (!rounded && cp.getMultipleOf() != null) { + pythonType.constrain(MULTIPLE_OF, cp.getMultipleOf()); + } + } + + private static int ceilValue(String value) { + return (int) Math.ceil(Double.parseDouble(value)); + } + + private static int floorValue(String value) { + return (int) Math.floor(Double.parseDouble(value)); + } + } }