From fa9c0cdbc4bb428b603cf7a082d15581959d203a Mon Sep 17 00:00:00 2001 From: scarf Date: Wed, 28 Jan 2026 16:40:21 +0900 Subject: [PATCH] feat(normalizer): add SORT_MODEL_PROPERTIES rule for deterministic output 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. --- docs/customization.md | 7 +++ .../codegen/OpenAPINormalizer.java | 21 ++++++-- .../codegen/OpenAPINormalizerTest.java | 53 +++++++++++++++++++ 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/docs/customization.md b/docs/customization.md index 3ed8d63b90c8..ba113c6705fc 100644 --- a/docs/customization.md +++ b/docs/customization.md @@ -723,3 +723,10 @@ Into this securityScheme: scheme: bearer type: http ``` + +- `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. + +Example: +``` +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 +``` diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index c91802e8e7fb..d396acc8566f 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -151,6 +151,9 @@ public class OpenAPINormalizer { boolean updateNumberToNullable; boolean updateBooleanToNullable; + // when set to true, sort model properties by name to ensure deterministic output + final String SORT_MODEL_PROPERTIES = "SORT_MODEL_PROPERTIES"; + // ============= end of rules ============= /** @@ -209,6 +212,7 @@ public OpenAPINormalizer(OpenAPI openAPI, Map inputRules) { ruleNames.add(SET_PRIMITIVE_TYPES_TO_NULLABLE); ruleNames.add(SIMPLIFY_ONEOF_ANYOF_ENUM); ruleNames.add(REMOVE_PROPERTIES_FROM_TYPE_OTHER_THAN_OBJECT); + ruleNames.add(SORT_MODEL_PROPERTIES); // rules that are default to true rules.put(SIMPLIFY_ONEOF_ANYOF, true); @@ -768,7 +772,7 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { } if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { - normalizeProperties(schema.getProperties(), visitedSchemas); + normalizeProperties(schema, visitedSchemas); } if (schema.getAdditionalProperties() != null) { @@ -777,7 +781,7 @@ public Schema normalizeSchema(Schema schema, Set visitedSchemas) { return schema; } else if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { - normalizeProperties(schema.getProperties(), visitedSchemas); + normalizeProperties(schema, visitedSchemas); } else if (schema.getAdditionalProperties() instanceof Schema) { // map normalizeMapSchema(schema); normalizeSchema((Schema) schema.getAdditionalProperties(), visitedSchemas); @@ -880,10 +884,19 @@ protected void normalizeIntegerSchema(Schema schema, Set visitedSchemas) processSetPrimitiveTypesToNullable(schema); } - protected void normalizeProperties(Map properties, Set visitedSchemas) { + protected void normalizeProperties(Schema schema, Set visitedSchemas) { + Map properties = schema.getProperties(); if (properties == null) { return; } + + // Sort properties by name if rule is enabled + if (getRule(SORT_MODEL_PROPERTIES)) { + Map sortedProperties = new TreeMap<>(properties); + schema.setProperties(sortedProperties); + properties = sortedProperties; + } + for (Map.Entry propertiesEntry : properties.entrySet()) { Schema property = propertiesEntry.getValue(); @@ -1089,7 +1102,7 @@ protected Schema normalizeAnyOf(Schema schema, Set visitedSchemas) { protected Schema normalizeComplexComposedSchema(Schema schema, Set visitedSchemas) { // loop through properties, if any if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { - normalizeProperties(schema.getProperties(), visitedSchemas); + normalizeProperties(schema, visitedSchemas); } processRemoveAnyOfOneOfAndKeepPropertiesOnly(schema); diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java index 2659cb81cace..e38373e8d81c 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/OpenAPINormalizerTest.java @@ -1306,6 +1306,59 @@ public void testRemoveXInternalFromInlineProperties() { assertNotNull(inlinePropertyAfter.getProperties().get("nestedNumber")); } + @Test + public void testSortModelProperties() { + // Create a schema with properties in non-alphabetical order + Schema schema = new ObjectSchema() + .addProperty("zebra", new StringSchema()) + .addProperty("apple", new StringSchema()) + .addProperty("mango", new IntegerSchema()); + + OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("TestModel", schema); + + // Verify original order (LinkedHashMap preserves insertion order) + List originalOrder = new ArrayList<>(schema.getProperties().keySet()); + assertEquals(originalOrder.get(0), "zebra"); + assertEquals(originalOrder.get(1), "apple"); + assertEquals(originalOrder.get(2), "mango"); + + // Apply normalizer with SORT_MODEL_PROPERTIES=true + Map options = new HashMap<>(); + options.put("SORT_MODEL_PROPERTIES", "true"); + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); + openAPINormalizer.normalize(); + + // Verify properties are now sorted alphabetically + Schema normalizedSchema = openAPI.getComponents().getSchemas().get("TestModel"); + List sortedOrder = new ArrayList<>(normalizedSchema.getProperties().keySet()); + assertEquals(sortedOrder.get(0), "apple"); + assertEquals(sortedOrder.get(1), "mango"); + assertEquals(sortedOrder.get(2), "zebra"); + } + + @Test + public void testSortModelPropertiesDisabledByDefault() { + // Create a schema with properties in non-alphabetical order + Schema schema = new ObjectSchema() + .addProperty("zebra", new StringSchema()) + .addProperty("apple", new StringSchema()) + .addProperty("mango", new IntegerSchema()); + + OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("TestModel", schema); + + // Apply normalizer without SORT_MODEL_PROPERTIES (default is false) + Map options = new HashMap<>(); + OpenAPINormalizer openAPINormalizer = new OpenAPINormalizer(openAPI, options); + openAPINormalizer.normalize(); + + // Verify properties retain original order + Schema normalizedSchema = openAPI.getComponents().getSchemas().get("TestModel"); + List order = new ArrayList<>(normalizedSchema.getProperties().keySet()); + assertEquals(order.get(0), "zebra"); + assertEquals(order.get(1), "apple"); + assertEquals(order.get(2), "mango"); + } + public static class RemoveRequiredNormalizer extends OpenAPINormalizer { public RemoveRequiredNormalizer(OpenAPI openAPI, Map inputRules) {