Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
b7dabdb
Add generated solution
ZhongpinWang Jul 21, 2026
cbddab1
fix: PMD Nonnull
ZhongpinWang Jul 22, 2026
42a0e60
Fix log
CharlesDuboisSAP Jul 22, 2026
ffe6a01
Remove unnecessary log warning and error
ZhongpinWang Jul 22, 2026
ad395cb
Log spec OAS version instead of mentioning 3.1 limitation
ZhongpinWang Jul 22, 2026
fe03631
Remove unused overload in oas version util
ZhongpinWang Jul 22, 2026
189c472
Add integration tests and fix exclusive min max in 3.0 and 3.1
ZhongpinWang Jul 22, 2026
65b4d10
Add unit test for pathItems
ZhongpinWang Jul 22, 2026
f14cdfa
Remove all Gap x in the code
ZhongpinWang Jul 22, 2026
0af7b8d
Add unit tests for normalizer and fix map logic for oas 3.0
ZhongpinWang Jul 22, 2026
88e1d34
Add all integration test for apache
ZhongpinWang Jul 22, 2026
97d5048
Remove warnings CustomJavaClientCodegen
CharlesDuboisSAP Jul 23, 2026
776c994
fix: nullable getter for non-required field
ZhongpinWang Jul 23, 2026
208a588
Fix the sibling description
ZhongpinWang Jul 23, 2026
39f1a95
Fix tests
ZhongpinWang Jul 23, 2026
8e1150c
Merge branch 'support-openapi-3.1' of https://github.com/SAP/cloud-sd…
ZhongpinWang Jul 23, 2026
b9cb23a
Moved components path items to integration test
ZhongpinWang Jul 23, 2026
ec48f91
Merge branch 'main' into support-openapi-3.1
ZhongpinWang Jul 23, 2026
986ced0
chore: revert the nonnull change back for non-required field
ZhongpinWang Jul 24, 2026
cb41ee4
Update test resources
ZhongpinWang Jul 27, 2026
3ab66c7
Remove all duplicated oas tests in apache integration test
ZhongpinWang Jul 30, 2026
8e3a83f
Merge branch 'main' into support-openapi-3.1
ZhongpinWang Jul 30, 2026
3e149d6
Remove analysis md
ZhongpinWang Jul 30, 2026
4719fbb
Formatting
ZhongpinWang Jul 30, 2026
bae1184
Add warning in parseOpenApiSpec for OAS 3.1
ZhongpinWang Jul 30, 2026
fbd2d19
formatting
ZhongpinWang Jul 30, 2026
2e861a3
release notes
CharlesDuboisSAP Jul 31, 2026
ecc67e7
Clean up
ZhongpinWang Jul 31, 2026
208c6ad
formatting
ZhongpinWang Jul 31, 2026
5fcd64c
Use tab 4 as indentation
ZhongpinWang Jul 31, 2026
dafe605
Reset the openapi-generator pom to main
ZhongpinWang Jul 31, 2026
0620345
Merge branch 'main' into support-openapi-3.1
ZhongpinWang Jul 31, 2026
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 @@ -34,8 +34,10 @@ void apply()

final var creators = new HashSet<CreatorDetails>();
for( final String candidate : Sets.union(m.anyOf, m.oneOf) ) {
final var creator = processCandidate(candidate);
creators.add(creator);
if( candidate != null ) {
final var creator = processCandidate(candidate);
creators.add(creator);
}
}

final var hasArray = creators.stream().anyMatch(CreatorDetails::isArray);
Expand All @@ -52,7 +54,7 @@ void apply()
log.warn(msg, m.name);
}
if( hasArray ) {
final var msg = "Field can be oneOf %d array types. Deserialization may not work as expected.";
final var msg = "Object contains oneOf with array types: {}. Deserialization may not work as expected.";
log.warn(msg, m.name);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import org.openapitools.codegen.CodegenProperty;
import org.openapitools.codegen.languages.JavaClientCodegen;
import org.openapitools.codegen.model.ModelMap;
import org.openapitools.codegen.model.ModelsMap;
import org.openapitools.codegen.model.OperationsMap;

import com.sap.cloud.sdk.datamodel.openapi.generator.model.GenerationConfiguration;
Expand All @@ -38,6 +39,8 @@ class CustomJavaClientCodegen extends JavaClientCodegen
{
private final GenerationConfiguration config;
private static final Predicate<String> DOUBLE_IS_PATTERN = Pattern.compile("^isIs[A-Z]").asPredicate();
// schemaName -> (propertyName -> sibling description) captured before normalization strips $ref context
private final Map<String, Map<String, String>> siblingDescriptions = new java.util.HashMap<>();

public CustomJavaClientCodegen( @Nonnull final GenerationConfiguration config )
{
Expand All @@ -47,6 +50,9 @@ public CustomJavaClientCodegen( @Nonnull final GenerationConfiguration config )
@Override
public void preprocessOpenAPI( @Nonnull final OpenAPI openAPI )
{
// Capture sibling descriptions on $ref property schemas before normalization resolves them away.
captureSiblingDescriptions(openAPI);

if( USE_EXCLUDE_PROPERTIES.isEnabled(config) ) {
final String[] exclusions = USE_EXCLUDE_PROPERTIES.getValue(config).trim().split("[,\\s]+");
for( final String exclusion : exclusions ) {
Expand All @@ -55,11 +61,12 @@ public void preprocessOpenAPI( @Nonnull final OpenAPI openAPI )
}
}

// OAS 3.1 documents may have no paths (webhooks-only or components-only).
if( USE_EXCLUDE_PATHS.isEnabled(config) ) {
final String[] exclusions = USE_EXCLUDE_PATHS.getValue(config).trim().split("[,\\s]+");
for( final String exclusion : exclusions ) {
if( !openAPI.getPaths().keySet().remove(exclusion) ) {
log.error("Could not remove path {}", exclusion);
if( openAPI.getPaths() != null ) {
for( final String exclusion : exclusions ) {
openAPI.getPaths().remove(exclusion);
}
}
}
Expand Down Expand Up @@ -137,6 +144,54 @@ protected void updateModelForComposedSchema(
}
}

@SuppressWarnings( { "rawtypes", "RedundantSuppression" } )
@Override
@Nonnull
public Map<String, ModelsMap> postProcessAllModels( @Nonnull final Map<String, ModelsMap> objs )
{
final Map<String, ModelsMap> result = super.postProcessAllModels(objs);

// Restore sibling descriptions lost during $ref resolution for primitive-typed properties.
for( final var schemaEntry : siblingDescriptions.entrySet() ) {
final ModelsMap modelsMap = result.get(schemaEntry.getKey());
if( modelsMap == null ) {
continue;
}
for( final ModelMap modelMap : modelsMap.getModels() ) {
for( final CodegenProperty prop : modelMap.getModel().vars ) {
final String siblingDesc = schemaEntry.getValue().get(prop.baseName);
if( siblingDesc != null ) {
prop.description = escapeText(siblingDesc);
prop.unescapedDescription = siblingDesc;
}
}
}
}
return result;
}

@SuppressWarnings( { "rawtypes", "unchecked" } )
private void captureSiblingDescriptions( @Nonnull final OpenAPI openAPI )
{
if( openAPI.getComponents() == null || openAPI.getComponents().getSchemas() == null ) {
return;
}
for( final var schemaEntry : openAPI.getComponents().getSchemas().entrySet() ) {
final Schema modelSchema = schemaEntry.getValue();
if( modelSchema.getProperties() == null ) {
continue;
}
for( final var propEntry : ((Map<String, Schema>) modelSchema.getProperties()).entrySet() ) {
final Schema propSchema = propEntry.getValue();
if( propSchema.get$ref() != null && propSchema.getDescription() != null ) {
siblingDescriptions
.computeIfAbsent(schemaEntry.getKey(), k -> new java.util.HashMap<>())
.put(propEntry.getKey(), propSchema.getDescription());
}
}
}
}

/**
* Remove property from specification.
*
Expand All @@ -147,7 +202,7 @@ protected void updateModelForComposedSchema(
* @param propertyName
* The name of the property to remove.
*/
@SuppressWarnings( { "rawtypes", "unchecked", "ReplaceInefficientStreamCount" } )
@SuppressWarnings( { "rawtypes", "unchecked" } )
private void preprocessRemoveProperty(
@Nonnull final OpenAPI openAPI,
@Nonnull final String schemaName,
Expand All @@ -161,7 +216,7 @@ private void preprocessRemoveProperty(
boolean removed = false;

final Predicate<Schema> remove =
s -> s != null && s.getProperties() != null && s.getProperties().remove(propertyName) != null;
s -> s.getProperties() != null && s.getProperties().remove(propertyName) != null;
final var schemasQueued = new LinkedList<Schema>();
final var schemasDone = new HashSet<Schema>();
schemasQueued.add(schema);
Expand Down Expand Up @@ -200,6 +255,11 @@ private void preprocessRemoveRedundancies( @Nonnull final OpenAPI openAPI )
final var refs = new LinkedHashSet<String>();
final var pattern = Pattern.compile("\\$ref: #/components/schemas/(\\w+)");

// OAS 3.1 documents may have no paths (webhooks-only or components-only).
if( openAPI.getPaths() == null || openAPI.getPaths().isEmpty() ) {
return;
Comment thread
CharlesDuboisSAP marked this conversation as resolved.
}

// find and queue schemas nested in paths
for( final var path : openAPI.getPaths().values() ) {
final var m = pattern.matcher(path.toString());
Expand All @@ -211,6 +271,20 @@ private void preprocessRemoveRedundancies( @Nonnull final OpenAPI openAPI )
}
}

// OAS 3.1 adds components/pathItems — traverse them for schema references too
final var pathItems = openAPI.getComponents() != null ? openAPI.getComponents().getPathItems() : null;
if( pathItems != null ) {
for( final var pathItem : pathItems.values() ) {
final var m = pattern.matcher(pathItem.toString());
while( m.find() ) {
final var name = m.group(1);
final var schema = openAPI.getComponents().getSchemas().get(name);
queue.add(schema);
refs.add(m.group(0).split(" ")[1]);
Comment thread
ZhongpinWang marked this conversation as resolved.
}
}
}

while( !queue.isEmpty() ) {
final var s = queue.remove();
if( s == null || !done.add(s) ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.sap.cloud.sdk.datamodel.openapi.generator;

import java.util.Map;
import java.util.Set;

import javax.annotation.Nonnull;

Expand All @@ -11,10 +12,13 @@
import io.swagger.v3.oas.models.media.Schema;

/**
* Fix Api client methods with oneOf primitive param to stay simplified from OpenAPI generator 7.22.0
* Fix Api client methods with oneOf primitive param to stay simplified from OpenAPI generator 7.22.0. Also adds OAS
* 3.1-aware normalisation: nullable warnings, example deprecation warnings, and contentEncoding/contentMediaType →
* format mapping for binary file uploads.
*/
public class CustomOpenAPINormalizer extends OpenAPINormalizer
{
private final boolean isOas31;

/**
* Initializes OpenAPI Normalizer with a set of rules
Expand All @@ -27,10 +31,12 @@ public class CustomOpenAPINormalizer extends OpenAPINormalizer
public CustomOpenAPINormalizer( final @Nonnull OpenAPI openAPI, final @Nonnull Map<String, String> inputRules )
{
super(openAPI, inputRules);
this.isOas31 = OasVersionUtil.isOas31(openAPI);
}

/**
* Normalize reference schema with allOf to support sibling properties
* Normalize reference schema with allOf to support sibling properties. Also warns on OAS 3.1 deprecated keywords
* when processing a 3.1 spec.
*
* @param schema
* Schema
Expand All @@ -46,15 +52,28 @@ protected void normalizeReferenceSchema( final @Nonnull Schema schema )
LOGGER.warn("Type(s) cleared (set to null) given $ref is set to {}.", schema.get$ref());
}

// warn when deprecated nullable: true is used in an OAS 3.1 spec
if( isOas31 && schema.getNullable() != null ) {
LOGGER
.warn(
"'nullable: true' is not a valid OAS 3.1 keyword on $ref schema '{}'. "
+ "Use anyOf: [{{$ref: \"...\"}}, {{type: \"null\"}}] instead.",
schema.get$ref());
}

if( schema.getTitle() != null
|| schema.getDescription() != null
|| schema.getNullable() != null
|| schema.getDefault() != null
|| schema.getDeprecated() != null
|| schema.getMaximum() != null
|| schema.getMinimum() != null
// OAS 3.0 boolean exclusiveMaximum/exclusiveMinimum
|| schema.getExclusiveMaximum() != null
|| schema.getExclusiveMinimum() != null
// OAS 3.1 numeric exclusiveMaximumValue/exclusiveMinimumValue
|| schema.getExclusiveMaximumValue() != null
|| schema.getExclusiveMinimumValue() != null
|| schema.getMaxItems() != null
|| schema.getMinItems() != null
|| schema.getMaxProperties() != null
Expand All @@ -65,6 +84,8 @@ protected void normalizeReferenceSchema( final @Nonnull Schema schema )
|| schema.getReadOnly() != null
|| schema.getExample() != null
|| (schema.getExamples() != null && !schema.getExamples().isEmpty())
// OAS 3.1 const keyword as $ref sibling
|| schema.getConst() != null
|| schema.getMultipleOf() != null
|| schema.getPattern() != null
|| (schema.getExtensions() != null && !schema.getExtensions().isEmpty()) ) {
Expand All @@ -87,4 +108,41 @@ protected void normalizeReferenceSchema( final @Nonnull Schema schema )
schema.set$ref(null);
}
}

/**
* Normalizes any schema (not just $ref schemas). Adds OAS 3.1 specific mappings:
* <ul>
* <li>warn on deprecated singular {@code example} keyword in OAS 3.1 schemas</li>
* <li>map {@code contentEncoding}/{@code contentMediaType} to {@code format} for binary file uploads</li>
* </ul>
*/
@Override
@Nonnull
@SuppressWarnings( { "rawtypes" } )
public Schema normalizeSchema( final @Nonnull Schema schema, final @Nonnull Set<Schema> visitedSchemas )
{
// warn on deprecated singular `example` in OAS 3.1 Schema Objects
if( isOas31 && schema.getExample() != null ) {
LOGGER
.warn(
"The 'example' keyword is deprecated in OAS 3.1 Schema Objects. "
+ "Use 'examples: [...]' (array form) instead.");
}

// map OAS 3.1 contentEncoding/contentMediaType to legacy format keyword
// so that downstream type-mapping (File -> byte[]) continues to work.
if( isOas31 && schema.getFormat() == null ) {
if( "base64".equalsIgnoreCase(schema.getContentEncoding()) ) {
schema.setFormat("byte");
} else if( schema.getContentEncoding() != null ) {
// Any other content encoding (e.g., "binary") → treat as binary
schema.setFormat("binary");
} else if( schema.getContentMediaType() != null ) {
// contentMediaType without contentEncoding → binary stream
schema.setFormat("binary");
Comment thread
ZhongpinWang marked this conversation as resolved.
}
}

return super.normalizeSchema(schema, visitedSchemas);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ private static void setGlobalSettings( @Nonnull final GenerationConfiguration co
log.warn("Parsing the specification yielded the following messages: {}", spec.getMessages());
}
final var result = spec.getOpenAPI();
log.info("Detected OpenAPI specification version {}.", result.getOpenapi());
if( OasVersionUtil.isOas31(result) ) {
log.warn("The input specification uses OpenAPI 3.1, which is an experimental feature. Use with caution.");
}
preprocessSpecification(result, config);
return result;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.sap.cloud.sdk.datamodel.openapi.generator;

import javax.annotation.Nonnull;

import io.swagger.v3.oas.models.OpenAPI;

final class OasVersionUtil
{
private OasVersionUtil()
{
}

static boolean isOas31( @Nonnull final OpenAPI openAPI )
{
final String version = openAPI.getOpenapi();
return version != null && version.startsWith("3.1");
Comment thread
CharlesDuboisSAP marked this conversation as resolved.
}
Comment thread
ZhongpinWang marked this conversation as resolved.
}
Loading