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
4 changes: 4 additions & 0 deletions bin/configs/csharp-generichost-net10.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ additionalProperties:
operationParameterSorting: alphabetical
treatWarningsAsErrors: true
warningsNotAsErrors: CS0612
injectModelVendorExtensions:
InjectedVendorExtensionsTest.potentiallyOverriddenPropertyToPrivate.x-setter-visibility: private
InjectedVendorExtensionsTest.potentiallyOverriddenPropertyToInternal.x-setter-visibility: internal
InjectedVendorExtensionsTest.potentiallyOverriddenPropertyToPublic.x-setter-visibility: public
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,15 @@ public class Generate extends OpenApiGeneratorCommand {
+ " You can also have multiple occurrences of this option.")
private List<String> operationIdNameMappings = new ArrayList<>();

@Option(
name = {"--inject-model-vendor-extensions"},
title = "inject model vendor extensions",
description = "injects vendor extensions into model classes or their properties."
+ " Class-level format: ModelName.x-extension-name=value."
+ " Property-level format: ModelName.propertyBaseName.x-extension-name=value."
+ " You can also have multiple occurrences of this option.")
private List<String> injectModelVendorExtensions = new ArrayList<>();

@Option(
name = {"--openapi-normalizer"},
title = "OpenAPI normalizer rules",
Expand Down Expand Up @@ -606,6 +615,7 @@ public void execute() {
applyModelNameMappingsKvpList(modelNameMappings, configurator);
applyEnumNameMappingsKvpList(enumNameMappings, configurator);
applyOperationIdNameMappingsKvpList(operationIdNameMappings, configurator);
applyInjectModelVendorExtensionsKvpList(injectModelVendorExtensions, configurator);
applyOpenapiNormalizerKvpList(openapiNormalizer, configurator);
applyTypeMappingsKvpList(typeMappings, configurator);
applyAdditionalPropertiesKvpList(additionalProperties, configurator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public final class GeneratorSettings implements Serializable {
private final Map<String, String> modelNameMappings;
private final Map<String, String> enumNameMappings;
private final Map<String, String> operationIdNameMappings;
private final Map<String, String> injectModelVendorExtensions;
Comment thread
devhl-labs marked this conversation as resolved.
private final Map<String, String> openapiNormalizer;
private final Set<String> languageSpecificPrimitives;
private final Set<String> openapiGeneratorIgnoreList;
Expand Down Expand Up @@ -327,6 +328,15 @@ public Map<String, String> getOperationIdNameMappings() {
return operationIdNameMappings;
}

/**
* Gets inject model vendor extensions.
*
* @return a map of ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name to extension value
*/
public Map<String, String> getInjectModelVendorExtensions() {
return injectModelVendorExtensions;
}

/**
* Gets OpenAPI normalizer rules
*
Expand Down Expand Up @@ -469,6 +479,7 @@ private GeneratorSettings(Builder builder) {
modelNameMappings = Collections.unmodifiableMap(builder.modelNameMappings);
enumNameMappings = Collections.unmodifiableMap(builder.enumNameMappings);
operationIdNameMappings = Collections.unmodifiableMap(builder.operationIdNameMappings);
injectModelVendorExtensions = Collections.unmodifiableMap(builder.injectModelVendorExtensions);
openapiNormalizer = Collections.unmodifiableMap(builder.openapiNormalizer);
languageSpecificPrimitives = Collections.unmodifiableSet(builder.languageSpecificPrimitives);
openapiGeneratorIgnoreList = Collections.unmodifiableSet(builder.openapiGeneratorIgnoreList);
Expand Down Expand Up @@ -550,6 +561,7 @@ public GeneratorSettings() {
modelNameMappings = Collections.unmodifiableMap(new HashMap<>(0));
enumNameMappings = Collections.unmodifiableMap(new HashMap<>(0));
operationIdNameMappings = Collections.unmodifiableMap(new HashMap<>(0));
injectModelVendorExtensions = Collections.unmodifiableMap(new HashMap<>(0));
openapiNormalizer = Collections.unmodifiableMap(new HashMap<>(0));
languageSpecificPrimitives = Collections.unmodifiableSet(new HashSet<>(0));
openapiGeneratorIgnoreList = Collections.unmodifiableSet(new HashSet<>(0));
Expand Down Expand Up @@ -630,6 +642,9 @@ public static Builder newBuilder(GeneratorSettings copy) {
if (copy.getOperationIdNameMappings() != null) {
builder.operationIdNameMappings.putAll(copy.getOperationIdNameMappings());
}
if (copy.getInjectModelVendorExtensions() != null) {
builder.injectModelVendorExtensions.putAll(copy.getInjectModelVendorExtensions());
}
if (copy.getOpenapiNormalizer() != null) {
builder.openapiNormalizer.putAll(copy.getOpenapiNormalizer());
}
Expand Down Expand Up @@ -684,6 +699,7 @@ public static final class Builder {
private Map<String, String> modelNameMappings;
private Map<String, String> enumNameMappings;
private Map<String, String> operationIdNameMappings;
private Map<String, String> injectModelVendorExtensions;
private Map<String, String> openapiNormalizer;
private Set<String> languageSpecificPrimitives;
private Set<String> openapiGeneratorIgnoreList;
Expand Down Expand Up @@ -712,6 +728,7 @@ public Builder() {
modelNameMappings = new HashMap<>();
enumNameMappings = new HashMap<>();
operationIdNameMappings = new HashMap<>();
injectModelVendorExtensions = new HashMap<>();
openapiNormalizer = new HashMap<>();
languageSpecificPrimitives = new HashSet<>();
openapiGeneratorIgnoreList = new HashSet<>();
Expand Down Expand Up @@ -1193,6 +1210,32 @@ public Builder withOperationIdNameMapping(String key, String value) {
return this;
}

/**
* Sets the {@code injectModelExtensions} and returns a reference to this Builder so that the methods can be chained together.
*
* @param injectModelExtensions the {@code injectModelExtensions} to set
* @return a reference to this Builder
*/
public Builder withInjectModelVendorExtensions(Map<String, String> injectModelVendorExtensions) {
this.injectModelVendorExtensions = injectModelVendorExtensions;
return this;
}

/**
* Sets a single {@code injectModelVendorExtension} and returns a reference to this Builder so that the methods can be chained together.
*
* @param key A key in the format ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name
* @param value The extension value
* @return a reference to this Builder
*/
public Builder withInjectModelVendorExtension(String key, String value) {
if (this.injectModelVendorExtensions == null) {
this.injectModelVendorExtensions = new HashMap<>();
}
this.injectModelVendorExtensions.put(key, value);
return this;
}

/**
* Sets the {@code openapiNormalizer} and returns a reference to this Builder so that the methods can be chained together.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ public interface CodegenConfig {

Map<String, String> operationIdNameMapping();

Map<String, String> injectModelVendorExtensions();

Map<String, String> openapiNormalizer();

Map<String, String> apiTemplateFiles();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ public class DefaultCodegen implements CodegenConfig {
protected Map<String, String> enumNameMapping = new HashMap<>();
// a map to store the mapping between operation id name and the name provided by the user
protected Map<String, String> operationIdNameMapping = new HashMap<>();
// a map to inject vendor extensions into model classes or their properties: key=ModelName.x-extension-name or ModelName.propertyBaseName.x-extension-name, value=extensionValue
protected Map<String, String> injectModelVendorExtensions = new HashMap<>();
// a map to store the rules in OpenAPI Normalizer
protected Map<String, String> openapiNormalizer = new HashMap<>();
@Setter
Expand Down Expand Up @@ -547,6 +549,42 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
}
}

// Inject vendor extensions from --inject-property-extensions into matching schema properties
if (!injectModelVendorExtensions.isEmpty()) {
Comment thread
devhl-labs marked this conversation as resolved.
for (Map.Entry<String, ModelsMap> entry : objs.entrySet()) {
CodegenModel model = ModelUtils.getModelByName(entry.getKey(), objs);
if (model == null) continue;

for (Map.Entry<String, String> extEntry : injectModelVendorExtensions.entrySet()) {
String[] parts = extEntry.getKey().split("\\.", 3);
if (parts.length < 2) continue;
String modelName = parts[0];
String extensionValue = extEntry.getValue();

if (!modelName.equals(entry.getKey())) continue;

if (parts.length == 2) {
// class-level extension: ModelName.x-extension-name
model.vendorExtensions.put(parts[1], extensionValue);
} else {
// property-level extension: ModelName.propertyBaseName.x-extension-name
String propertyBaseName = parts[1];
String extensionName = parts[2];
List<List<CodegenProperty>> allPropertyLists = Arrays.asList(
Comment thread
devhl-labs marked this conversation as resolved.
model.vars, model.allVars, model.readWriteVars, model.requiredVars,
model.optionalVars, model.parentVars, model.readOnlyVars, model.nonNullableVars);
for (List<CodegenProperty> properties : allPropertyLists) {
for (CodegenProperty property : properties) {
if (propertyBaseName.equals(property.baseName)) {
property.vendorExtensions.put(extensionName, extensionValue);
}
}
}
}
}
}
}

if (this.useOneOfInterfaces) {
// First, add newly created oneOf interfaces
for (CodegenModel cm : addOneOfInterfaces) {
Expand Down Expand Up @@ -1366,6 +1404,11 @@ public Map<String, String> operationIdNameMapping() {
return operationIdNameMapping;
}

@Override
public Map<String, String> injectModelVendorExtensions() {
return injectModelVendorExtensions;
}

@Override
public Map<String, String> openapiNormalizer() {
return openapiNormalizer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public class CodegenConfigurator {
private Map<String, String> modelNameMappings = new HashMap<>();
private Map<String, String> enumNameMappings = new HashMap<>();
private Map<String, String> operationIdNameMappings = new HashMap<>();
private Map<String, String> injectModelVendorExtensions = new HashMap<>();
private Map<String, String> openapiNormalizer = new HashMap<>();
private Set<String> languageSpecificPrimitives = new HashSet<>();
private Set<String> openapiGeneratorIgnoreList = new HashSet<>();
Expand Down Expand Up @@ -149,6 +150,9 @@ public static CodegenConfigurator fromFile(String configFile, Module... modules)
if (generatorSettings.getOperationIdNameMappings() != null) {
configurator.operationIdNameMappings.putAll(generatorSettings.getOperationIdNameMappings());
}
if (generatorSettings.getInjectModelVendorExtensions() != null) {
configurator.injectModelVendorExtensions.putAll(generatorSettings.getInjectModelVendorExtensions());
}
if (generatorSettings.getOpenapiNormalizer() != null) {
configurator.openapiNormalizer.putAll(generatorSettings.getOpenapiNormalizer());
}
Expand Down Expand Up @@ -295,6 +299,18 @@ public CodegenConfigurator addOperationIdNameMapping(String key, String value) {
return this;
}

public CodegenConfigurator addInjectModelVendorExtension(String key, String value) {
this.injectModelVendorExtensions.put(key, value);
generatorSettingsBuilder.withInjectModelVendorExtension(key, value);
return this;
}

public CodegenConfigurator setInjectModelVendorExtensions(Map<String, String> extensions) {
this.injectModelVendorExtensions = extensions;
generatorSettingsBuilder.withInjectModelVendorExtensions(extensions);
return this;
}

public CodegenConfigurator addOpenapiNormalizer(String key, String value) {
this.openapiNormalizer.put(key, value);
generatorSettingsBuilder.withOpenapiNormalizer(key, value);
Expand Down Expand Up @@ -815,6 +831,7 @@ public ClientOptInput toClientOptInput() {
config.modelNameMapping().putAll(generatorSettings.getModelNameMappings());
config.enumNameMapping().putAll(generatorSettings.getEnumNameMappings());
config.operationIdNameMapping().putAll(generatorSettings.getOperationIdNameMappings());
config.injectModelVendorExtensions().putAll(generatorSettings.getInjectModelVendorExtensions());
config.openapiNormalizer().putAll(generatorSettings.getOpenapiNormalizer());
config.languageSpecificPrimitives().addAll(generatorSettings.getLanguageSpecificPrimitives());
config.openapiGeneratorIgnoreList().addAll(generatorSettings.getOpenapiGeneratorIgnoreList());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,19 @@ public static void applyOpenapiNormalizerKvp(String openapiNormalizer, CodegenCo
}
}

public static void applyInjectModelVendorExtensionsKvpList(List<String> injectModelVendorExtensions, CodegenConfigurator configurator) {
for (String propString : injectModelVendorExtensions) {
applyInjectModelVendorExtensionsKvp(propString, configurator);
}
}

public static void applyInjectModelVendorExtensionsKvp(String injectModelVendorExtensions, CodegenConfigurator configurator) {
final Map<String, String> map = createMapFromKeyValuePairs(injectModelVendorExtensions);
for (Map.Entry<String, String> entry : map.entrySet()) {
configurator.addInjectModelVendorExtension(entry.getKey().trim(), entry.getValue().trim());
}
}

public static void applyTypeMappingsKvpList(List<String> typeMappings, CodegenConfigurator configurator) {
for (String propString : typeMappings) {
applyTypeMappingsKvp(propString, configurator);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,19 @@ protected void patchProperty(Map<String, CodegenModel> enumRefs, CodegenModel mo
if (property.datatypeWithEnum.equals("decimal")) {
property.isDecimal = true;
}

// Normalize x-setter-visibility:
// "public" -> remove extension, set isReadOnly=false (public setter = default, no modifier needed)
// any other value -> set isReadOnly=true (template emits "{{.}} set;" using the extension value)
Object setterVisibilityObj = property.vendorExtensions.get("x-setter-visibility");
if (setterVisibilityObj instanceof String) {
if ("public".equals(setterVisibilityObj)) {
property.vendorExtensions.remove("x-setter-visibility");
property.isReadOnly = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The model's mutability flag (X_MODEL_IS_MUTABLE), which controls whether the generated C# model gets a public or internal constructor, is computed in postProcessModels() at line 647 before patchProperty() runs, so the new x-setter-visibility normalization that flips property.isReadOnly isn't reflected in that decision. For the 'public' override direction (a spec readOnly property turned into a public setter), the model can still be generated with an internal constructor even though its property now has a public setter, preventing external callers from actually using it. Consider recomputing model mutability after this normalization (or computing it after the per-property patches) so the constructor visibility matches the effective setter access.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractCSharpCodegen.java, line 894:

<comment>The model's mutability flag (X_MODEL_IS_MUTABLE), which controls whether the generated C# model gets a public or internal constructor, is computed in postProcessModels() at line 647 before patchProperty() runs, so the new x-setter-visibility normalization that flips property.isReadOnly isn't reflected in that decision. For the 'public' override direction (a spec readOnly property turned into a public setter), the model can still be generated with an internal constructor even though its property now has a public setter, preventing external callers from actually using it. Consider recomputing model mutability after this normalization (or computing it after the per-property patches) so the constructor visibility matches the effective setter access.</comment>

<file context>
@@ -883,6 +883,19 @@ protected void patchProperty(Map<String, CodegenModel> enumRefs, CodegenModel mo
+        if (setterVisibilityObj instanceof String) {
+            if ("public".equals(setterVisibilityObj)) {
+                property.vendorExtensions.remove("x-setter-visibility");
+                property.isReadOnly = false;
+            } else {
+                property.isReadOnly = true;
</file context>

} else {
property.isReadOnly = true;
}
}
}

@Override
Expand Down
Loading
Loading