Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/docker-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to DockerHub
uses: docker/login-action@v4
uses: docker/login-action@v4.5.2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/samples-java-sbt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
distribution: 'temurin'
java-version: 17
- name: Setup sbt launcher
uses: sbt/setup-sbt@v1
uses: sbt/setup-sbt@v1.5.4
- name: Cache maven dependencies
uses: actions/cache@v6
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/samples-scala-client.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:
distribution: 'temurin'
java-version: 11
- name: Setup sbt launcher
uses: sbt/setup-sbt@v1
uses: sbt/setup-sbt@v1.5.4
- name: Cache maven dependencies
uses: actions/cache@v6
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/samples-scala-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
distribution: 'temurin'
java-version: 11
- name: Setup sbt launcher
uses: sbt/setup-sbt@v1
uses: sbt/setup-sbt@v1.5.4
- name: Cache maven dependencies
uses: actions/cache@v6
env:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,7 @@ public boolean specVersionGreaterThanOrEqualTo310(OpenAPI openAPI) {
@Override
public void setOpenAPI(OpenAPI openAPI) {
if (specVersionGreaterThanOrEqualTo310(openAPI)) {
LOGGER.warn(UNSUPPORTED_V310_SPEC_MSG);
once(LOGGER).warn(UNSUPPORTED_V310_SPEC_MSG);
}
this.openAPI = openAPI;
// Set global settings such that helper functions in ModelUtils can lookup the value
Expand Down Expand Up @@ -1264,7 +1264,7 @@ public String encodePath(String input) {
*/
@Override
public String escapeUnsafeCharacters(String input) {
LOGGER.warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape " +
once(LOGGER).warn("escapeUnsafeCharacters should be overridden in the code generator with proper logic to escape " +
"unsafe characters");
// doing nothing by default and code generator should implement
// the logic to prevent code injection
Expand All @@ -1281,7 +1281,7 @@ public String escapeUnsafeCharacters(String input) {
*/
@Override
public String escapeQuotationMark(String input) {
LOGGER.warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape " +
once(LOGGER).warn("escapeQuotationMark should be overridden in the code generator with proper logic to escape " +
"single/double quote");
return input.replace("\"", "\\\"");
}
Expand Down Expand Up @@ -2924,7 +2924,7 @@ protected void updateModelForComposedSchema(CodegenModel m, Schema schema, Map<S
addAdditionPropertiesToCodeGenModel(m, schema);
}

if (Boolean.TRUE.equals(schema.getNullable())) {
if (ModelUtils.isNullable(schema)) {
m.isNullable = Boolean.TRUE;
}

Expand Down Expand Up @@ -3924,8 +3924,8 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
if (p.getWriteOnly() != null) {
property.isWriteOnly = p.getWriteOnly();
}
if (p.getNullable() != null) {
property.isNullable = p.getNullable();
if (ModelUtils.isNullable(p)) {
property.isNullable = true;
}

if (p.getExtensions() != null && !p.getExtensions().isEmpty()) {
Expand Down Expand Up @@ -3975,12 +3975,8 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
}
}

// set isNullable using nullable or x-nullable in the schema
if (referencedSchema.getNullable() != null) {
property.isNullable = referencedSchema.getNullable();
} else if (referencedSchema.getExtensions() != null &&
referencedSchema.getExtensions().containsKey(X_NULLABLE)) {
property.isNullable = (Boolean) referencedSchema.getExtensions().get(X_NULLABLE);
if (ModelUtils.isNullable(referencedSchema)) {
property.isNullable = true;
}

final XML referencedSchemaXml = referencedSchema.getXml();
Expand Down Expand Up @@ -4079,10 +4075,8 @@ public CodegenProperty fromProperty(String name, Schema p, boolean required, boo
if (original != null) {
p = original;
// evaluate common attributes if defined in the top level
if (p.getNullable() != null) {
property.isNullable = p.getNullable();
} else if (p.getExtensions() != null && p.getExtensions().containsKey(X_NULLABLE)) {
property.isNullable = (Boolean) p.getExtensions().get(X_NULLABLE);
if (ModelUtils.isNullable(p)) {
property.isNullable = true;
}

if (p.getReadOnly() != null) {
Expand Down Expand Up @@ -5286,7 +5280,7 @@ public CodegenParameter fromParameter(Parameter parameter, Set<String> imports)
codegenParameter.setTypeProperties(parameterSchema, openAPI);
codegenParameter.setComposedSchemas(getComposedSchemas(parameterSchema));

if (Boolean.TRUE.equals(parameterSchema.getNullable())) { // use nullable defined in the spec
if (ModelUtils.isNullable(parameterSchema)) { // use nullable defined in the spec
codegenParameter.isNullable = true;
}

Expand Down Expand Up @@ -8060,7 +8054,9 @@ public CodegenParameter fromRequestBody(RequestBody body, Set<String> imports, S
if (original.getNullable() != null) {
codegenParameter.isNullable = original.getNullable();
} else if (original.getExtensions() != null && original.getExtensions().containsKey(X_NULLABLE)) {
codegenParameter.isNullable = (Boolean) original.getExtensions().get(X_NULLABLE);
codegenParameter.isNullable = Boolean.parseBoolean(String.valueOf(original.getExtensions().get(X_NULLABLE)));
} else if (ModelUtils.isNullable(original)) {
codegenParameter.isNullable = true;
}

if (original.getExtensions() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -998,8 +998,8 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
}
normalizeProperties(schema, visitedSchemas);
} else if (schema.getAdditionalProperties() instanceof Schema) { // map
normalizeMapSchema(schema);
Schema additionalProperties = (Schema) schema.getAdditionalProperties();
Schema result = normalizeMapSchema(schema);
Schema additionalProperties = (Schema) result.getAdditionalProperties();
if (getRule(NORMALIZE_31SPEC) && ModelUtils.isNullTypeSchema(openAPI, additionalProperties)) {
// OAS 3.1 allows a map value schema of `type: "null"` (e.g.
// `additionalProperties: { type: "null" }`). There's no OAS 3.0 equivalent type,
Expand All @@ -1008,15 +1008,17 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
// generated as a normal (nullable) object instead.
Schema anyTypeNullable = new Schema();
anyTypeNullable.setNullable(true);
schema.setAdditionalProperties(anyTypeNullable);
result.setAdditionalProperties(anyTypeNullable);
} else {
Schema normalized = normalizeSchema(additionalProperties, visitedSchemas);
if (getRule(NORMALIZE_31SPEC)) {
// capture the normalized value schema (e.g. an OAS 3.1 `type: [array, "null"]`
// value is rewritten to a proper array schema), which would otherwise be lost.
schema.setAdditionalProperties(normalized);
result.setAdditionalProperties(normalized);
}
}

return result;
} else if (schema instanceof BooleanSchema) {
normalizeBooleanSchema(schema, visitedSchemas);
} else if (schema instanceof IntegerSchema) {
Expand Down Expand Up @@ -1105,7 +1107,8 @@ protected Schema normalizeArraySchema(Schema schema) {
}

protected Schema normalizeMapSchema(Schema schema) {
return processSetMapToNullable(schema);
Schema result = processNormalize31Spec(schema, new HashSet<>());
return processSetMapToNullable(result);
}

protected Schema normalizeSimpleSchema(Schema schema, Set<Schema> visitedSchemas) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,7 @@ public ModelsMap postProcessModels(ModelsMap objs) {
if (useOptional) {
for (ModelMap modelMap : objs.getModels()) {
CodegenModel model = modelMap.getModel();
boolean hasOptionalProperties = false;

boolean shouldUseOptional;

Expand All @@ -650,9 +651,12 @@ public ModelsMap postProcessModels(ModelsMap objs) {
for (CodegenProperty prop : model.vars) {
if (!prop.required && !prop.dataType.startsWith("Optional<")) {
wrapPropertyWithOptional(prop);
hasOptionalProperties = true;
}
}
}

model.vendorExtensions.put("x-has-optional-properties", hasOptionalProperties);
}
}

Expand All @@ -667,13 +671,16 @@ private void wrapPropertyWithOptional(CodegenProperty property) {

boolean hasNullableSuffix = property.dataType.endsWith("?");
String baseType = hasNullableSuffix ? property.dataType.substring(0, property.dataType.length() - 1) : property.dataType;
property.vendorExtensions.put("x-unwrapped-datatype-nullable", baseType + "?");
property.dataType = "Optional<" + baseType + "?" + ">";

if (property.datatypeWithEnum != null && !property.datatypeWithEnum.startsWith("Optional<")) {
hasNullableSuffix = property.datatypeWithEnum.endsWith("?");
baseType = hasNullableSuffix ? property.datatypeWithEnum.substring(0, property.datatypeWithEnum.length() - 1) : property.datatypeWithEnum;
property.datatypeWithEnum = "Optional<" + baseType + "?" + ">";
}

property.isNullable = false;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import org.openapitools.codegen.model.ModelsMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.MessageFormatter;

import java.math.BigDecimal;
import java.net.URI;
Expand Down Expand Up @@ -1297,6 +1298,39 @@ public static List<Schema> getAllSchemas(OpenAPI openAPI) {
return allSchemas;
}

/**
* Return the list of all schemas in the entire OpenAPI document, including inline schemas
* defined in path operations (request bodies, responses, parameters, headers, callbacks)
* and schemas under components/schemas. Results are deduplicated by identity.
* This is a superset of {@link #getAllSchemas(OpenAPI)}.
*
* @param openAPI specification
* @return schemas a deduplicated list of all schemas in the document
*/
public static List<Schema> getAllSchemasInDocument(OpenAPI openAPI) {
List<Schema> allSchemas = new ArrayList<Schema>();
Set<Schema> seen = Collections.newSetFromMap(new IdentityHashMap<>());

// Visit schemas reachable from paths (inline + $ref targets)
visitOpenAPI(openAPI, (s, mimeType) -> {
if (seen.add(s)) {
allSchemas.add(s);
}
});

// Also visit components/schemas entries not reachable from any path
List<String> refSchemas = new ArrayList<String>();
getSchemas(openAPI).forEach((key, schema) -> {
visitSchema(openAPI, schema, null, refSchemas, (s, mimeType) -> {
if (seen.add(s)) {
allSchemas.add(s);
}
});
});

return allSchemas;
}

/**
* If a RequestBody contains a reference to another RequestBody with '$ref', returns the referenced RequestBody if it is found or the actual RequestBody in the other cases.
*
Expand Down Expand Up @@ -1588,7 +1622,7 @@ public static Schema unaliasSchema(OpenAPI openAPI,
Schema ref = allSchemas.get(simpleRef);
if (ref == null) {
if (!isRefToSchemaWithProperties(schema.get$ref())) {
once(LOGGER).warn("{} is not defined", schema.get$ref());
once(LOGGER).warn(MessageFormatter.format("{} is not defined", schema.get$ref()).getMessage());
}
return schema;
} else if (isEnumSchema(ref)) {
Expand Down Expand Up @@ -1968,6 +2002,7 @@ public static boolean isNullable(Schema schema) {
if (schema.getExtensions() != null && schema.getExtensions().get(X_NULLABLE) != null) {
return Boolean.parseBoolean(schema.getExtensions().get(X_NULLABLE).toString());
}

// In OAS 3.1, the recommended way to define a nullable property or object is to use oneOf.
if (isComposedSchema(schema)) {
return isNullableComposedSchema(schema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ static void caffeineCache(Ticker ticker, int expireMillis) {
.build();
}

/**
* This implementation currently only supports single-argument string literal log methods (e.g. {@link Logger#debug(String)}).
*
* @param logger The logger that should only log once for single-argument string literal log methods.
* @return The {@link OnceLogger}
*/
public static Logger once(Logger logger) {
try {
if (Boolean.parseBoolean(GlobalSettings.getProperty(ENABLE_ONCE_LOGGER_PROPERTY, "true"))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.openapitools.codegen.CodegenConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.helpers.MessageFormatter;

import java.net.MalformedURLException;
import java.net.URL;
Expand All @@ -37,14 +38,21 @@
public class URLPathUtils {

private static final Logger LOGGER = LoggerFactory.getLogger(URLPathUtils.class);
private static final String SERVER_NOT_SPECIFIED =
"'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [{}] for server URL [{}]";
private static final String SCHEME_NOT_DEFINED = "'scheme' not defined in the spec (2.0). Default to [http] for server URL [{}]";
private static final String NO_SERVER_INFO = "Server information not defined in the spec. Default to {}.";
private static final String INVALID_URL = "Not a valid URL: {}. Default to {}.";
public static final String LOCAL_HOST = "http://localhost";
public static final Pattern VARIABLE_PATTERN = Pattern.compile("(?<!\\$)\\{([^\\}]+)\\}");
public static final Pattern URL_WITH_SCHEME = Pattern.compile("[a-zA-Z][0-9a-zA-Z.+\\-]+://.+");

// TODO: This should probably be moved into generator/workflow type rather than a static like this.
public static URL getServerURL(OpenAPI openAPI, Map<String, String> userDefinedVariables) {
final List<Server> servers = openAPI.getServers();
if (servers == null || servers.isEmpty()) {
once(LOGGER).warn("Server information seems not defined in the spec. Default to {}.", LOCAL_HOST);
String message = MessageFormatter.format(NO_SERVER_INFO, LOCAL_HOST).getMessage();
once(LOGGER).warn(message);
return getDefaultUrl();
}
// TODO need a way to obtain all server URLs
Expand All @@ -67,7 +75,8 @@ public static URL getServerURL(final Server server, final Map<String, String> us
try {
return new URL(url);
} catch (MalformedURLException e) {
once(LOGGER).warn("Not valid URL: {}. Default to {}.", server.getUrl(), LOCAL_HOST);
String malformedUrl = MessageFormatter.format(INVALID_URL, server.getUrl(), LOCAL_HOST).getMessage();
once(LOGGER).warn(malformedUrl);
}
}
return getDefaultUrl();
Expand Down Expand Up @@ -206,19 +215,22 @@ private static String sanitizeUrl(String url) {
if (url != null) {
if (url.startsWith("//")) {
url = "http:" + url;
once(LOGGER).warn("'scheme' not defined in the spec (2.0). Default to [http] for server URL [{}]", url);
String missingScheme = MessageFormatter.format(SCHEME_NOT_DEFINED, url).getMessage();
once(LOGGER).warn(missingScheme);
} else if (url.startsWith("/")) {
url = LOCAL_HOST + url;
once(LOGGER).info("'host' (OAS 2.0) or 'servers' (OAS 3.0) not defined in the spec. Default to [{}] for server URL [{}]", LOCAL_HOST, url);
} else if (!url.matches("[a-zA-Z][0-9a-zA-Z.+\\-]+://.+")) {
String serverDefaultToLocalhost = MessageFormatter.format(SERVER_NOT_SPECIFIED, LOCAL_HOST, url).getMessage();
once(LOGGER).info(serverDefaultToLocalhost);
} else if (!URL_WITH_SCHEME.matcher(url).matches()) {
// Add http scheme for urls without a scheme.
// 2.0 spec is restricted to the following schemes: "http", "https", "ws", "wss"
// 3.0 spec does not have an enumerated list of schemes
// This regex attempts to capture all schemes in IANA example schemes which
// can have alpha-numeric characters and [.+-]. Examples are here:
// can have alphanumeric characters and [.+-]. Examples are here:
// https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml
url = "http://" + url;
once(LOGGER).warn("'scheme' not defined in the spec (2.0). Default to [http] for server URL [{}]", url);
String missingScheme = MessageFormatter.format(SCHEME_NOT_DEFINED, url).getMessage();
once(LOGGER).warn(missingScheme);
}
}
return url;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,29 @@ public ValidationResult validate(OpenAPI specification) {
validationResult.consume(schemaValidations.validate(wrapper));
});

// Per-occurrence check: default value not in enum.
// Uses getAllSchemasInDocument to also cover inline schemas in path operations.
if (ruleConfiguration.isEnableRecommendations()
&& ruleConfiguration.isEnableDefaultNotInEnumRecommendation()) {
ValidationRule defaultNotInEnumRule = ValidationRule.create(Severity.WARNING,
"Schema has default value not in enum",
"While technically valid, a default outside the enum may cause "
+ "generators to emit incorrect default values.",
s -> ValidationRule.Pass.empty());
for (Schema schema : ModelUtils.getAllSchemasInDocument(specification)) {
List<?> enumList = schema.getEnum();
Object defaultValue = schema.getDefault();
if (enumList != null && !enumList.isEmpty()
&& defaultValue != null
&& !enumList.contains(defaultValue)) {
validationResult.addResult(Validated.invalid(defaultNotInEnumRule,
String.format(Locale.ROOT,
"Schema has default value '%s' not in enum %s",
defaultValue, enumList)));
}
}
}

List<Parameter> parameters = new ArrayList<>(50);

Paths paths = specification.getPaths();
Expand Down
Loading
Loading