Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1318,7 +1318,8 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
List<CodegenParameter> params = operation.allParams;

for (CodegenParameter cp : params) {
PydanticType pydantic = new PydanticType(
PydanticType pydantic = getPydanticParameterType(
cp,
modelImports,
exampleImports,
postponedModelImports,
Expand Down Expand Up @@ -1414,6 +1415,23 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
return objs;
}

protected PydanticType getPydanticParameterType(CodegenParameter parameter,
Set<String> modelImports,
Set<String> exampleImports,
Set<String> postponedModelImports,
Set<String> postponedExampleImports,
PythonImports moduleImports,
String classname) {
return new PydanticType(
modelImports,
exampleImports,
postponedModelImports,
postponedExampleImports,
moduleImports,
classname
);
}


@Override
public void postProcessParameter(CodegenParameter parameter) {
Expand Down Expand Up @@ -1781,7 +1799,7 @@ public String asTypeValue(PythonImports imports) {
* entries will be automatically removed.
*
* */
class PythonImports {
protected class PythonImports {
private Map<String, Set<String>> imports;

public PythonImports() {
Expand Down Expand Up @@ -1827,22 +1845,22 @@ public boolean isEmpty() {
}
}

class PydanticType {
protected 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";
protected static final String LESS_THAN = "lt";
protected static final String GREATER_THAN = "gt";
protected static final String GREATER_OR_EQUAL_TO = "ge";
protected static final String LESS_OR_EQUAL_TO = "le";
protected static final String TYPING = "typing";

private static final String DECIMAL = "Decimal";
protected static final String DECIMAL = "Decimal";

private Set<String> modelImports;
private Set<String> exampleImports;
private Set<String> postponedModelImports;
private Set<String> postponedExampleImports;
private PythonImports moduleImports;
private String classname;
protected Set<String> modelImports;
protected Set<String> exampleImports;
protected Set<String> postponedModelImports;
protected Set<String> postponedExampleImports;
protected PythonImports moduleImports;
protected String classname;

public PydanticType(
Set<String> modelImports,
Expand All @@ -1860,7 +1878,7 @@ public PydanticType(
this.classname = classname;
}

private PythonType arrayType(IJsonSchemaValidationProperties cp) {
protected PythonType arrayType(IJsonSchemaValidationProperties cp) {
PythonType pt = new PythonType();
if (cp.getMaxItems() != null) {
pt.constrain("max_length", cp.getMaxItems());
Expand All @@ -1887,7 +1905,7 @@ private PythonType arrayType(IJsonSchemaValidationProperties cp) {
return pt;
}

private PythonType collectionItemType(CodegenProperty itemCp) {
protected PythonType collectionItemType(CodegenProperty itemCp) {
PythonType itemPt = getType(itemCp);
if (itemCp != null && !itemPt.type.equals("Any") && itemCp.isNullable) {
moduleImports.add(TYPING, "Optional");
Expand All @@ -1898,7 +1916,7 @@ private PythonType collectionItemType(CodegenProperty itemCp) {
return itemPt;
}

private PythonType stringType(IJsonSchemaValidationProperties cp) {
protected PythonType stringType(IJsonSchemaValidationProperties cp) {

if (cp.getHasValidation()) {
PythonType pt = new PythonType("str");
Expand Down Expand Up @@ -1929,15 +1947,15 @@ private PythonType stringType(IJsonSchemaValidationProperties cp) {
}
}

private PythonType mapType(IJsonSchemaValidationProperties cp) {
protected PythonType mapType(IJsonSchemaValidationProperties cp) {
moduleImports.add(TYPING, "Dict");
PythonType pt = new PythonType("Dict");
pt.addTypeParam(new PythonType("str"));
pt.addTypeParam(collectionItemType(cp.getItems()));
return pt;
}

private PythonType numberType(IJsonSchemaValidationProperties cp) {
protected PythonType numberType(IJsonSchemaValidationProperties cp) {
if (cp.getHasValidation()) {
PythonType floatt = new PythonType("float");
PythonType intt = new PythonType("int");
Expand Down Expand Up @@ -1998,7 +2016,7 @@ private PythonType numberType(IJsonSchemaValidationProperties cp) {
}
}

private PythonType intType(IJsonSchemaValidationProperties cp) {
protected PythonType intType(IJsonSchemaValidationProperties cp) {
if (cp.getHasValidation()) {
PythonType pt = new PythonType("int");
// e.g. conint(ge=10, le=100, strict=True)
Expand All @@ -2011,7 +2029,7 @@ private PythonType intType(IJsonSchemaValidationProperties cp) {
}
}

private PythonType binaryType(IJsonSchemaValidationProperties cp) {
protected PythonType binaryType(IJsonSchemaValidationProperties cp) {
if (cp.getHasValidation()) {
PythonType bytest = new PythonType("bytes");
PythonType strt = new PythonType("str");
Expand Down Expand Up @@ -2075,12 +2093,12 @@ private PythonType binaryType(IJsonSchemaValidationProperties cp) {
}
}

private PythonType boolType(IJsonSchemaValidationProperties cp) {
protected PythonType boolType(IJsonSchemaValidationProperties cp) {
moduleImports.add(PYDANTIC, "StrictBool");
return new PythonType("StrictBool");
}

private PythonType decimalType(IJsonSchemaValidationProperties cp) {
protected PythonType decimalType(IJsonSchemaValidationProperties cp) {
PythonType pt = new PythonType(DECIMAL);
moduleImports.add("decimal", DECIMAL);

Expand All @@ -2093,12 +2111,12 @@ private PythonType decimalType(IJsonSchemaValidationProperties cp) {
return pt;
}

private PythonType anyType(IJsonSchemaValidationProperties cp) {
protected PythonType anyType(IJsonSchemaValidationProperties cp) {
moduleImports.add(TYPING, "Any");
return new PythonType("Any");
}

private PythonType dateType(IJsonSchemaValidationProperties cp) {
protected PythonType dateType(IJsonSchemaValidationProperties cp) {
if (cp.getIsDate()) {
moduleImports.add("datetime", "date");
}
Expand All @@ -2109,20 +2127,20 @@ private PythonType dateType(IJsonSchemaValidationProperties cp) {
return new PythonType(cp.getDataType());
}

private PythonType uuidType(IJsonSchemaValidationProperties cp) {
protected PythonType uuidType(IJsonSchemaValidationProperties cp) {
moduleImports.add("uuid", "UUID");
return new PythonType("UUID");
}

private PythonType modelType(IJsonSchemaValidationProperties cp) {
protected PythonType modelType(IJsonSchemaValidationProperties cp) {
// add model prefix
hasModelsToImport = true;
modelImports.add(cp.getDataType());
exampleImports.add(cp.getDataType());
return new PythonType(cp.getDataType());
}

private PythonType fromCommon(IJsonSchemaValidationProperties cp) {
protected PythonType fromCommon(IJsonSchemaValidationProperties cp) {
if (cp == null) {
// if codegen property (e.g. map/dict of undefined type) is null, default to string
LOGGER.warn("Codegen property is null (e.g. map/dict of undefined type). Default to typing.Any.");
Expand Down Expand Up @@ -2170,7 +2188,7 @@ public String generatePythonType(CodegenProperty cp) {
return this.finalizeType(cp, pt);
}

private PythonType getType(CodegenProperty cp) {
protected PythonType getType(CodegenProperty cp) {
PythonType result = fromCommon(cp);

/* comment out the following since Literal requires python 3.8
Expand Down Expand Up @@ -2276,7 +2294,7 @@ public String generatePythonType(CodegenParameter cp) {
return this.finalizeType(cp, pt);
}

private PythonType getType(CodegenParameter cp) {
protected PythonType getType(CodegenParameter cp) {
// TODO: cleanup
PythonType result = fromCommon(cp);

Expand Down Expand Up @@ -2305,7 +2323,7 @@ private PythonType getType(CodegenParameter cp) {
return result;
}

private void applyConstraints(PythonType pythonType, IJsonSchemaValidationProperties cp) {
protected void applyConstraints(PythonType pythonType, IJsonSchemaValidationProperties cp) {
if (cp.getMaximum() != null) {
if (cp.getExclusiveMaximum()) {
pythonType.constrain(LESS_THAN, cp.getMaximum(), false);
Expand Down Expand Up @@ -2341,4 +2359,123 @@ private String finalizeType(CodegenParameter cp, PythonType pt) {
return pt.asTypeConstraintWithAnnotations(moduleImports);
}
}

/**
* Pydantic type generator for values that arrive over the wire as strings — server-bound request
* parameters in path, query, and header position. These rely on Pydantic's automatic coercion
* (e.g. {@code "3" -> 3}); the strict types emitted by the base {@link PydanticType}
* ({@code StrictInt}/{@code StrictStr}/{@code StrictFloat}, {@code strict=True}) disable that
* coercion and make FastAPI reject otherwise-valid requests with a 422. See issue #21905.
*
* <p>Request bodies and models are <em>not</em> wire-string values — they carry real JSON types —
* so they keep the strict base behaviour.
*/
protected class PydanticCoercibleType extends PydanticType {
public PydanticCoercibleType(
Set<String> modelImports,
Set<String> exampleImports,
Set<String> postponedModelImports,
Set<String> postponedExampleImports,
PythonImports moduleImports,
String classname
) {
super(modelImports, exampleImports, postponedModelImports, postponedExampleImports, moduleImports, classname);
}

@Override
protected PythonType stringType(IJsonSchemaValidationProperties cp) {
if (cp.getHasValidation()) {
PythonType pt = new PythonType("str");
if (cp.getMaxLength() != null) {
pt.constrain("max_length", cp.getMaxLength());
}
if (cp.getMinLength() != null) {
pt.constrain("min_length", cp.getMinLength());
}
if (cp.getPattern() != null) {
moduleImports.add(PYDANTIC, "field_validator");
}
return pt;
} else if ("password".equals(cp.getFormat())) { // TODO avoid using format, use `is` boolean flag instead
moduleImports.add(PYDANTIC, "SecretStr");
return new PythonType("SecretStr");
}

return new PythonType("str");
}

@Override
protected PythonType numberType(IJsonSchemaValidationProperties cp) {
if (cp.getHasValidation()) {
PythonType floatt = new PythonType("float");
PythonType intt = new PythonType("int");

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())));
} else {
floatt.constrain(LESS_OR_EQUAL_TO, cp.getMaximum(), false);
intt.constrain(LESS_OR_EQUAL_TO, (int) Math.floor(Double.valueOf(cp.getMaximum())));
}
}
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())));
} else {
floatt.constrain(GREATER_OR_EQUAL_TO, cp.getMinimum(), false);
intt.constrain(GREATER_OR_EQUAL_TO, (int) Math.ceil(Double.valueOf(cp.getMinimum())));
}
}
if (cp.getMultipleOf() != null) {
floatt.constrain("multiple_of", cp.getMultipleOf());
}

if ("Union[StrictFloat, StrictInt]".equals(mapNumberTo)) {
moduleImports.add(TYPING, "Union");
PythonType pt = new PythonType("Union");
pt.addTypeParam(floatt);
pt.addTypeParam(intt);
return pt;
}

return floatt;
} else if ("Union[StrictFloat, StrictInt]".equals(mapNumberTo)) {
moduleImports.add(TYPING, "Union");
PythonType pt = new PythonType("Union");
pt.addTypeParam(new PythonType("float"));
pt.addTypeParam(new PythonType("int"));
return pt;
}

return new PythonType("float");
}

@Override
protected PythonType intType(IJsonSchemaValidationProperties cp) {
PythonType pt = new PythonType("int");
if (cp.getHasValidation()) {
applyConstraints(pt, cp);
}
return pt;
}

@Override
protected PythonType boolType(IJsonSchemaValidationProperties cp) {
return new PythonType("bool");
}

@Override
protected PythonType decimalType(IJsonSchemaValidationProperties cp) {
PythonType pt = new PythonType(DECIMAL);
moduleImports.add("decimal", DECIMAL);

if (cp.getHasValidation()) {
applyConstraints(pt, cp);
}

return pt;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,38 @@ public String getTypeDeclaration(Schema p) {
return super.getTypeDeclaration(p);
}

@Override
protected PydanticType getPydanticParameterType(CodegenParameter parameter,
Set<String> modelImports,
Set<String> exampleImports,
Set<String> postponedModelImports,
Set<String> postponedExampleImports,
PythonImports moduleImports,
String classname) {
// Path/query/header values always arrive as strings on the wire and rely on Pydantic
// coercion, so they must not use strict types. Body params keep the strict default.
if (parameter.isQueryParam || parameter.isPathParam || parameter.isHeaderParam) {
return new PydanticCoercibleType(
modelImports,
exampleImports,
postponedModelImports,
postponedExampleImports,
moduleImports,
classname
);
}

return super.getPydanticParameterType(
parameter,
modelImports,
exampleImports,
postponedModelImports,
postponedExampleImports,
moduleImports,
classname
);
}

@Override
public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<ModelMap> allModels) {
super.postProcessOperationsWithModels(objs, allModels);
Expand Down
Loading
Loading