Skip to content

Commit f7ac63a

Browse files
authored
feat(normalizer): add SORT_MODEL_PROPERTIES rule for deterministic output (#22836)
Add new OpenAPINormalizer rule SORT_MODEL_PROPERTIES that sorts schema properties alphabetically by name. This ensures deterministic code generation output regardless of property ordering in the source spec. The rule: - Uses TreeMap to sort properties by natural string order - Applies at the OpenAPI normalization stage, working for all generators - Is opt-in (defaults to false) to maintain backward compatibility Usage: openapi-generator generate --openapi-normalizer SORT_MODEL_PROPERTIES=true ... Fixes non-deterministic property ordering that could cause spurious diffs in generated code when the source schema order varies.
1 parent c0d555b commit f7ac63a

3 files changed

Lines changed: 77 additions & 4 deletions

File tree

docs/customization.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -723,3 +723,10 @@ Into this securityScheme:
723723
scheme: bearer
724724
type: http
725725
```
726+
727+
- `SORT_MODEL_PROPERTIES`: When set to true, model properties will be sorted alphabetically by name. This ensures deterministic code generation output regardless of property ordering in the source spec.
728+
729+
Example:
730+
```
731+
java -jar modules/openapi-generator-cli/target/openapi-generator-cli.jar generate -g java -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml -o /tmp/java-okhttp/ --openapi-normalizer SORT_MODEL_PROPERTIES=true
732+
```

modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ public class OpenAPINormalizer {
151151
boolean updateNumberToNullable;
152152
boolean updateBooleanToNullable;
153153

154+
// when set to true, sort model properties by name to ensure deterministic output
155+
final String SORT_MODEL_PROPERTIES = "SORT_MODEL_PROPERTIES";
156+
154157
// ============= end of rules =============
155158

156159
/**
@@ -209,6 +212,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map<String, String> inputRules) {
209212
ruleNames.add(SET_PRIMITIVE_TYPES_TO_NULLABLE);
210213
ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM);
211214
ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT);
215+
ruleNames.add(SORT_MODEL_PROPERTIES);
212216

213217
// rules that are default to true
214218
rules.put(SIMPLIFY_ONEOF_ANYOF, true);
@@ -768,7 +772,7 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
768772
}
769773

770774
if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
771-
normalizeProperties(schema.getProperties(), visitedSchemas);
775+
normalizeProperties(schema, visitedSchemas);
772776
}
773777

774778
if (schema.getAdditionalProperties() != null) {
@@ -777,7 +781,7 @@ public Schema normalizeSchema(Schema schema, Set<Schema> visitedSchemas) {
777781

778782
return schema;
779783
} else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
780-
normalizeProperties(schema.getProperties(), visitedSchemas);
784+
normalizeProperties(schema, visitedSchemas);
781785
} else if (schema.getAdditionalProperties() instanceof Schema) { // map
782786
normalizeMapSchema(schema);
783787
normalizeSchema((Schema) schema.getAdditionalProperties(), visitedSchemas);
@@ -880,10 +884,19 @@ protected void normalizeIntegerSchema(Schema schema, Set<Schema> visitedSchemas)
880884
processSetPrimitiveTypesToNullable(schema);
881885
}
882886

883-
protected void normalizeProperties(Map<String, Schema> properties, Set<Schema> visitedSchemas) {
887+
protected void normalizeProperties(Schema schema, Set<Schema> visitedSchemas) {
888+
Map<String, Schema> properties = schema.getProperties();
884889
if (properties == null) {
885890
return;
886891
}
892+
893+
// Sort properties by name if rule is enabled
894+
if (getRule(SORT_MODEL_PROPERTIES)) {
895+
Map<String, Schema> sortedProperties = new TreeMap<>(properties);
896+
schema.setProperties(sortedProperties);
897+
properties = sortedProperties;
898+
}
899+
887900
for (Map.Entry<String, Schema> propertiesEntry : properties.entrySet()) {
888901
Schema property = propertiesEntry.getValue();
889902

@@ -1089,7 +1102,7 @@ protected Schema normalizeAnyOf(Schema schema, Set<Schema> visitedSchemas) {
10891102
protected Schema normalizeComplexComposedSchema(Schema schema, Set<Schema> visitedSchemas) {
10901103
// loop through properties, if any
10911104
if (schema.getProperties() != null && !schema.getProperties().isEmpty()) {
1092-
normalizeProperties(schema.getProperties(), visitedSchemas);
1105+
normalizeProperties(schema, visitedSchemas);
10931106
}
10941107

10951108
processRemoveAnyOfOneOfAndKeepPropertiesOnly(schema);

modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,6 +1306,59 @@ public void testRemoveXInternalFromInlineProperties() {
13061306
assertNotNull(inlinePropertyAfter.getProperties().get("nestedNumber"));
13071307
}
13081308

1309+
@Test
1310+
public void testSortModelProperties() {
1311+
// Create a schema with properties in non-alphabetical order
1312+
Schema schema = new ObjectSchema()
1313+
.addProperty("zebra", new StringSchema())
1314+
.addProperty("apple", new StringSchema())
1315+
.addProperty("mango", new IntegerSchema());
1316+
1317+
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("TestModel", schema);
1318+
1319+
// Verify original order (LinkedHashMap preserves insertion order)
1320+
List<String> originalOrder = new ArrayList<>(schema.getProperties().keySet());
1321+
assertEquals(originalOrder.get(0), "zebra");
1322+
assertEquals(originalOrder.get(1), "apple");
1323+
assertEquals(originalOrder.get(2), "mango");
1324+
1325+
// Apply normalizer with SORT_MODEL_PROPERTIES=true
1326+
Map<String, String> options = new HashMap<>();
1327+
options.put("SORT_MODEL_PROPERTIES", "true");
1328+
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
1329+
openAPINormalizer.normalize();
1330+
1331+
// Verify properties are now sorted alphabetically
1332+
Schema normalizedSchema = openAPI.getComponents().getSchemas().get("TestModel");
1333+
List<String> sortedOrder = new ArrayList<>(normalizedSchema.getProperties().keySet());
1334+
assertEquals(sortedOrder.get(0), "apple");
1335+
assertEquals(sortedOrder.get(1), "mango");
1336+
assertEquals(sortedOrder.get(2), "zebra");
1337+
}
1338+
1339+
@Test
1340+
public void testSortModelPropertiesDisabledByDefault() {
1341+
// Create a schema with properties in non-alphabetical order
1342+
Schema schema = new ObjectSchema()
1343+
.addProperty("zebra", new StringSchema())
1344+
.addProperty("apple", new StringSchema())
1345+
.addProperty("mango", new IntegerSchema());
1346+
1347+
OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("TestModel", schema);
1348+
1349+
// Apply normalizer without SORT_MODEL_PROPERTIES (default is false)
1350+
Map<String, String> options = new HashMap<>();
1351+
OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options);
1352+
openAPINormalizer.normalize();
1353+
1354+
// Verify properties retain original order
1355+
Schema normalizedSchema = openAPI.getComponents().getSchemas().get("TestModel");
1356+
List<String> order = new ArrayList<>(normalizedSchema.getProperties().keySet());
1357+
assertEquals(order.get(0), "zebra");
1358+
assertEquals(order.get(1), "apple");
1359+
assertEquals(order.get(2), "mango");
1360+
}
1361+
13091362
public static class RemoveRequiredNormalizer extends OpenAPINormalizer {
13101363

13111364
public RemoveRequiredNormalizer(OpenAPI openAPI, Map<String, String> inputRules) {

0 commit comments

Comments
 (0)