Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## unreleased

- Casing - Configure characters to keep
- CLI: Fix YAML output to preserve x-version number formatting

## [1.31.0] - 2026-04-12
Expand Down
104 changes: 70 additions & 34 deletions openapi-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -889,23 +889,24 @@ async function openapiChangeCase(oaObj, options) {
let jsonObj = JSON.parse(JSON.stringify(oaObj)); // Deep copy of the schema object
let defaultCasing = {}; // JSON.parse(fs.readFileSync(__dirname + "/defaultFilter.json", 'utf8'))
let casingSet = Object.assign({}, defaultCasing, options.casingSet);
const getKeepChars = target => casingSet[`${target}KeepChars`];

let debugCasingStep = ''; // uncomment // debugCasingStep below to see which sort part is triggered

// Could add a default for all types pretty easily.
const changeCasingKeyPlans = {
query: casingSet.componentsParametersQuery,
path: casingSet.componentsParametersPath,
header: casingSet.componentsParametersHeader,
cookie: casingSet.componentsParametersCookie
query: {case: casingSet.componentsParametersQuery, keep: getKeepChars('componentsParametersQuery')},
path: {case: casingSet.componentsParametersPath, keep: getKeepChars('componentsParametersPath')},
header: {case: casingSet.componentsParametersHeader, keep: getKeepChars('componentsParametersHeader')},
cookie: {case: casingSet.componentsParametersCookie, keep: getKeepChars('componentsParametersCookie')}
};

// Could add a default for all types pretty easily.
const changeCasingNamePlans = {
query: casingSet.parametersQuery,
path: casingSet.parametersPath,
header: casingSet.parametersHeader,
cookie: casingSet.parametersCookie
query: {case: casingSet.parametersQuery, keep: getKeepChars('parametersQuery')},
path: {case: casingSet.parametersPath, keep: getKeepChars('parametersPath')},
header: {case: casingSet.parametersHeader, keep: getKeepChars('parametersHeader')},
cookie: {case: casingSet.parametersCookie, keep: getKeepChars('parametersCookie')}
};

// Initiate components tracking
Expand All @@ -920,17 +921,17 @@ async function openapiChangeCase(oaObj, options) {
// Change components/schemas - names
if (this.path[1] === 'schemas' && this.path.length === 2 && casingSet.componentsSchemas) {
// debugCasingStep = 'Casing - components/schemas - names'
this.update(changeObjKeysCase(node, casingSet.componentsSchemas));
this.update(changeObjKeysCase(node, casingSet.componentsSchemas, getKeepChars('componentsSchemas')));
}
// Change components/examples - names
if (this.path[1] === 'examples' && this.path.length === 2 && casingSet.componentsExamples) {
// debugCasingStep = 'Casing - components/examples - names'
this.update(changeObjKeysCase(node, casingSet.componentsExamples));
this.update(changeObjKeysCase(node, casingSet.componentsExamples, getKeepChars('componentsExamples')));
}
// Change components/headers - names
if (this.path[1] === 'headers' && this.path.length === 2 && casingSet.componentsHeaders) {
// debugCasingStep = 'Casing - components/headers - names'
this.update(changeObjKeysCase(node, casingSet.componentsHeaders));
this.update(changeObjKeysCase(node, casingSet.componentsHeaders, getKeepChars('componentsHeaders')));
}
// Change components/parameters - in:query/in:headers/in:path/in:cookie - key
if (
Expand All @@ -943,41 +944,46 @@ async function openapiChangeCase(oaObj, options) {
const parameterFoundIn = orgObj[key].in;
if (orgObj[key].in && changeCasingKeyPlans.hasOwnProperty(parameterFoundIn)) {
const changeCasingKeyPlan = changeCasingKeyPlans[parameterFoundIn];
if (changeCasingKeyPlan) {
if (changeCasingKeyPlan && changeCasingKeyPlan.case) {
// debugCasingStep = `Casing - components/parameters - in:${parameterFoundIn} - key`
const newKey = changeCase(key, changeCasingKeyPlan);
const newKey = changeCase(key, changeCasingKeyPlan.case, changeCasingKeyPlan.keep);
comps.parameters[key] = newKey;
return {[newKey]: orgObj[key]};
}
}
return {[key]: orgObj[key]};
});
this.update(Object.assign({}, ...replacedItems));
}
// Change components/parameters - query/header/path/cookie name
if (this.path[1] === 'parameters' && this.path.length === 3) {
if (node.in && changeCasingNamePlans.hasOwnProperty(node.in)) {
const changeCasingNamePlan = changeCasingNamePlans[node.in];
if (changeCasingNamePlan) {
if (changeCasingNamePlan.case) {
// debugCasingStep = `Casing - path > parameters/${node.in} - name`
node.name = changeCase(node.name, changeCasingNamePlan);
node.name = changeCase(node.name, changeCasingNamePlan.case, changeCasingNamePlan.keep);
this.update(node);
}
}
}
// Change components/responses - names
if (this.path[1] === 'responses' && this.path.length === 2 && casingSet.componentsResponses) {
// debugCasingStep = 'Casing - components/responses - names'
this.update(changeObjKeysCase(node, casingSet.componentsResponses));
this.update(changeObjKeysCase(node, casingSet.componentsResponses, getKeepChars('componentsResponses')));
}
// Change components/requestBodies - names
if (this.path[1] === 'requestBodies' && this.path.length === 2 && casingSet.componentsRequestBodies) {
// debugCasingStep = 'Casing - components/requestBodies - names'
this.update(changeObjKeysCase(node, casingSet.componentsRequestBodies));
this.update(
changeObjKeysCase(node, casingSet.componentsRequestBodies, getKeepChars('componentsRequestBodies'))
);
}
// Change components/securitySchemes - names
if (this.path[1] === 'securitySchemes' && this.path.length === 2 && casingSet.componentsSecuritySchemes) {
// debugCasingStep = 'Casing - components/securitySchemes - names'
this.update(changeObjKeysCase(node, casingSet.componentsSecuritySchemes));
this.update(
changeObjKeysCase(node, casingSet.componentsSecuritySchemes, getKeepChars('componentsSecuritySchemes'))
);
}
}
});
Expand All @@ -988,15 +994,29 @@ async function openapiChangeCase(oaObj, options) {
if (this.key === '$ref') {
if (node.startsWith('#/components/schemas/') && casingSet.componentsSchemas) {
const compName = node.replace('#/components/schemas/', '');
this.update(`#/components/schemas/${changeCase(compName, casingSet.componentsSchemas)}`);
this.update(
`#/components/schemas/${changeCase(compName, casingSet.componentsSchemas, getKeepChars('componentsSchemas'))}`
);
}
if (node.startsWith('#/components/examples/') && casingSet.componentsExamples) {
const compName = node.replace('#/components/examples/', '');
this.update(`#/components/examples/${changeCase(compName, casingSet.componentsExamples)}`);
this.update(
`#/components/examples/${changeCase(
compName,
casingSet.componentsExamples,
getKeepChars('componentsExamples')
)}`
);
}
if (node.startsWith('#/components/responses/') && casingSet.componentsResponses) {
const compName = node.replace('#/components/responses/', '');
this.update(`#/components/responses/${changeCase(compName, casingSet.componentsResponses)}`);
this.update(
`#/components/responses/${changeCase(
compName,
casingSet.componentsResponses,
getKeepChars('componentsResponses')
)}`
);
}
if (node.startsWith('#/components/parameters/')) {
const compName = node.replace('#/components/parameters/', '');
Expand All @@ -1006,37 +1026,51 @@ async function openapiChangeCase(oaObj, options) {
}
if (node.startsWith('#/components/headers/') && casingSet.componentsHeaders) {
const compName = node.replace('#/components/headers/', '');
this.update(`#/components/headers/${changeCase(compName, casingSet.componentsHeaders)}`);
this.update(
`#/components/headers/${changeCase(compName, casingSet.componentsHeaders, getKeepChars('componentsHeaders'))}`
);
}
if (node.startsWith('#/components/requestBodies/') && casingSet.componentsRequestBodies) {
const compName = node.replace('#/components/requestBodies/', '');
this.update(`#/components/requestBodies/${changeCase(compName, casingSet.componentsRequestBodies)}`);
this.update(
`#/components/requestBodies/${changeCase(
compName,
casingSet.componentsRequestBodies,
getKeepChars('componentsRequestBodies')
)}`
);
}
if (node.startsWith('#/components/securitySchemes/') && casingSet.componentsSecuritySchemes) {
const compName = node.replace('#/components/securitySchemes/', '');
this.update(`#/components/securitySchemes/${changeCase(compName, casingSet.componentsSecuritySchemes)}`);
this.update(
`#/components/securitySchemes/${changeCase(
compName,
casingSet.componentsSecuritySchemes,
getKeepChars('componentsSecuritySchemes')
)}`
);
}
}

// Change operationId
if (this.key === 'operationId' && casingSet.operationId && this.path[0] === 'paths' && this.path.length === 4) {
// debugCasingStep = 'Casing - Single field - OperationId'
this.update(changeCase(node, casingSet.operationId));
this.update(changeCase(node, casingSet.operationId, getKeepChars('operationId')));
}
// Change summary
if (this.key === 'summary' && casingSet.summary) {
// debugCasingStep = 'Casing - Single field - summary'
this.update(changeCase(node, casingSet.summary));
this.update(changeCase(node, casingSet.summary, getKeepChars('summary')));
}
// Change description
if (this.key === 'description' && casingSet.description) {
// debugCasingStep = 'Casing - Single field - description'
this.update(changeCase(node, casingSet.description));
this.update(changeCase(node, casingSet.description, getKeepChars('description')));
}
// Change paths > examples - name
if (this.path[0] === 'paths' && this.key === 'examples' && casingSet.componentsExamples) {
// debugCasingStep = 'Casing - Single field - examples name'
this.update(changeObjKeysCase(node, casingSet.componentsExamples));
this.update(changeObjKeysCase(node, casingSet.componentsExamples, getKeepChars('componentsExamples')));
}
// Change components/schemas - properties
if (
Expand All @@ -1048,12 +1082,12 @@ async function openapiChangeCase(oaObj, options) {
this.parent.key !== 'value'
) {
// debugCasingStep = 'Casing - components/schemas - properties name'
this.update(changeObjKeysCase(node, casingSet.properties));
this.update(changeObjKeysCase(node, casingSet.properties, getKeepChars('properties')));
}
// Change components/schemas - required properties
if (this.path[1] === 'schemas' && this.parent.key === 'required' && casingSet.properties) {
// debugCasingStep = 'Casing - components/schemas - required properties'
this.update(changeCase(node, casingSet.properties));
this.update(changeCase(node, casingSet.properties, getKeepChars('properties')));
}
// Change paths > schema - properties
if (
Expand All @@ -1065,12 +1099,14 @@ async function openapiChangeCase(oaObj, options) {
this.parent.key !== 'value'
) {
// debugCasingStep = 'Casing - paths > schema - properties name'
this.update(changeObjKeysCase(node, casingSet.properties));
this.update(changeObjKeysCase(node, casingSet.properties, getKeepChars('properties')));
}
// Change security - keys
if (this.path[0] === 'paths' && this.key === 'security' && isArray(node) && casingSet.componentsSecuritySchemes) {
// debugCasingStep = 'Casing - path > - security'
this.update(changeArrayObjKeysCase(node, casingSet.componentsSecuritySchemes));
this.update(
changeArrayObjKeysCase(node, casingSet.componentsSecuritySchemes, getKeepChars('componentsSecuritySchemes'))
);
}
// Change parameters - name
if (this.path[0] === 'paths' && this.key === 'parameters' && changeParametersCasingEnabled(casingSet)) {
Expand All @@ -1081,9 +1117,9 @@ async function openapiChangeCase(oaObj, options) {
for (let i = 0; i < params.length; i++) {
if (params[i].in && changeCasingNamePlans.hasOwnProperty(params[i].in)) {
const changeCasingNamePlan = changeCasingNamePlans[params[i].in];
if (changeCasingNamePlan) {
if (changeCasingNamePlan.case) {
// debugCasingStep = 'Casing - path > parameters/query- name'
params[i].name = changeCase(params[i].name, changeCasingNamePlan);
params[i].name = changeCase(params[i].name, changeCasingNamePlan.case, changeCasingNamePlan.keep);
}
}
}
Expand Down
28 changes: 23 additions & 5 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -1327,11 +1327,17 @@ In this example, the customCasing.yaml file would contain the desired casing pre

```yaml
operationId: snake_case
operationIdKeepChars:
- .
properties: camelCase
propertiesKeepChars:
- .

parametersQuery: kebab-case
parametersHeader: kebab-case
parametersPath: snake_case
parametersQueryKeepChars:
- .

componentsExamples: PascalCase
componentsSchemas: camelCase
Expand All @@ -1343,18 +1349,30 @@ componentsSecuritySchemes: PascalCase
componentsParametersQuery: snake_case
componentsParametersHeader: kebab-case
componentsParametersPath: camelCase
componentsParametersQueryKeepChars:
- .
```

**Casing Options:**
In the customCasing.yaml, you can define the casing style for various OpenAPI properties, allowing you to customize the appearance of your document consistently.

- `operationId`: Defines the casing for operation IDs. Example: snake_case, PascalCase, or camelCase.
- `properties`: Sets the casing for properties within components. Example: camelCase.
- `parametersQuery`, `parametersHeader`, `parametersPath`: Define different casing styles for parameters based on their location (query, header, path). Example: snake_case, kebab-case.
- and many more
For every casing option, you can optionally define a matching `*KeepChars` option. This follows the same per-element configuration model as the casing rules themselves: different OpenAPI elements can have different casing conventions, and they may also need to preserve different separator characters. For example, query parameters may preserve dots for nested parameter names, while component schemas may preserve dots for versioned names.

- `operationId`: Defines the casing for operation IDs. Example: `snake_case`, `PascalCase`, or `camelCase`.
- `operationIdKeepChars`: Keeps selected characters while casing `operationId`. Example: `.` for dotted identifiers.
- `properties`: Sets the casing for properties within schemas. Example: `camelCase`.
- `propertiesKeepChars`: Keeps selected characters while casing schema properties. Example: `.` for dotted property names.
- `parametersQuery`, `parametersHeader`, `parametersPath`, `parametersCookie`: Define different casing styles for parameters based on their location. Example: `camelCase` for query parameters, `kebab-case` for headers, or `snake_case` for path parameters.
- `parametersQueryKeepChars`, `parametersHeaderKeepChars`, `parametersPathKeepChars`, `parametersCookieKeepChars`: Keep selected characters while casing inline parameter names.
- `componentsSchemas`, `componentsExamples`, `componentsHeaders`, `componentsResponses`, `componentsRequestBodies`, `componentsSecuritySchemes`: Define casing styles for component keys.
- `componentsSchemasKeepChars`, `componentsExamplesKeepChars`, `componentsHeadersKeepChars`, `componentsResponsesKeepChars`, `componentsRequestBodiesKeepChars`, `componentsSecuritySchemesKeepChars`: Keep selected characters while casing component keys.
- `componentsParametersQuery`, `componentsParametersHeader`, `componentsParametersPath`, `componentsParametersCookie`: Define casing styles for reusable parameters in `components.parameters`, based on their `in` value.
- `componentsParametersQueryKeepChars`, `componentsParametersHeaderKeepChars`, `componentsParametersPathKeepChars`, `componentsParametersCookieKeepChars`: Keep selected characters while casing reusable parameter keys in `components.parameters`.

See [OpenAPI formatting configuration options](#openapi-formatting-configuration-options) for the full list of casing options

Note: When custom keep characters are configured, they are merged with the default characters that `openapi-format` already preserves: `$` and `@`. To disable the default preserved characters for direct `changeCase` usage, pass an empty array as the custom keep characters.

## CLI Bundle & Split usage

- **Bundling**: Create a self-contained OpenAPI file that can be used for documentation generation or API validation tools that don't support external references.
Expand Down Expand Up @@ -1713,4 +1731,4 @@ The casing options available in `openapi-format` are powered by the excellent [c
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains logo." width="200px">
</a>

Special thanks to [JetBrains](https://www.jetbrains.com/) for their continuous sponsorship of this project over the last 3 years, and for their support to open-source software (OSS) initiatives.
Special thanks to [JetBrains](https://www.jetbrains.com/) for their continuous sponsorship of this project over the last 3 years, and for their support to open-source software (OSS) initiatives.
Loading
Loading