diff --git a/CHANGELOG.md b/CHANGELOG.md
index c798a79..2113396 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/openapi-format.js b/openapi-format.js
index 84101fa..5f2ee0a 100644
--- a/openapi-format.js
+++ b/openapi-format.js
@@ -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
@@ -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 (
@@ -943,13 +944,14 @@ 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));
}
@@ -957,9 +959,9 @@ async function openapiChangeCase(oaObj, options) {
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);
}
}
@@ -967,17 +969,21 @@ async function openapiChangeCase(oaObj, options) {
// 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'))
+ );
}
}
});
@@ -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/', '');
@@ -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 (
@@ -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 (
@@ -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)) {
@@ -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);
}
}
}
diff --git a/readme.md b/readme.md
index fbf69e8..831ec8f 100644
--- a/readme.md
+++ b/readme.md
@@ -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
@@ -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.
@@ -1713,4 +1731,4 @@ The casing options available in `openapi-format` are powered by the excellent [c
-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.
\ No newline at end of file
+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.
diff --git a/skills/openapi-format/references/feature-matrix.md b/skills/openapi-format/references/feature-matrix.md
index 82ddc6d..f0bfa53 100644
--- a/skills/openapi-format/references/feature-matrix.md
+++ b/skills/openapi-format/references/feature-matrix.md
@@ -4,20 +4,20 @@ This matrix summarizes CLI/config options, defaults, and notable interactions.
## Input and output
-| Option | Type | Default | Effect | Notes |
-|---|---|---|---|---|
-| `oaFile` | argument | required unless overlay `extends` | Input OpenAPI/AsyncAPI source | Supports local path and remote URL.
-| `--output`, `-o` | path | none | Write output to file or split root | Required for `--split`.
-| `--json` | boolean | false | Print stdout as JSON | Affects stdout formatting when no output file is given.
-| `--yaml` | boolean | false | Print stdout as YAML | Affects stdout formatting when no output file is given.
+| Option | Type | Default | Effect | Notes |
+|------------------|----------|-----------------------------------|------------------------------------|---------------------------------------------------------|
+| `oaFile` | argument | required unless overlay `extends` | Input OpenAPI/AsyncAPI source | Supports local path and remote URL. |
+| `--output`, `-o` | path | none | Write output to file or split root | Required for `--split`. |
+| `--json` | boolean | false | Print stdout as JSON | Affects stdout formatting when no output file is given. |
+| `--yaml` | boolean | false | Print stdout as YAML | Affects stdout formatting when no output file is given. |
## Config loading and precedence
-| Source | Applied when | Priority |
-|---|---|---|
-| `.openapiformatrc` in CWD | only when `--configFile` is absent | lowest |
-| `--configFile` | when provided | middle |
-| CLI flags | always | highest |
+| Source | Applied when | Priority |
+|---------------------------|------------------------------------|----------|
+| `.openapiformatrc` in CWD | only when `--configFile` is absent | lowest |
+| `--configFile` | when provided | middle |
+| CLI flags | always | highest |
Normalization behavior:
- `--no-sort` sets `sort=false`.
@@ -33,37 +33,37 @@ Defaults with explicit fallback logic:
## Sorting
-| Option | Type | Default | Effect | Notes |
-|---|---|---|---|---|
-| `--sortFile`, `-s` | path | `defaultSort.json` | Field order priorities | Used when `sort=true`.
-| `--no-sort` | boolean | false | Disable sorting | Skips sort stage entirely.
-| `--sortComponentsFile` | path | none | Alphabetize listed component groups | See `defaultSortComponents.json`.
-| `--sortComponentsProps` | boolean | false | Alphabetize schema properties in components | Scope is `components.schemas.*.properties`.
-| `sortSet.sortPathsBy` | enum | `original` | Path order mode | Values: `original`, `path`, `tags`.
+| Option | Type | Default | Effect | Notes |
+|-------------------------|---------|--------------------|---------------------------------------------|---------------------------------------------|
+| `--sortFile`, `-s` | path | `defaultSort.json` | Field order priorities | Used when `sort=true`. |
+| `--no-sort` | boolean | false | Disable sorting | Skips sort stage entirely. |
+| `--sortComponentsFile` | path | none | Alphabetize listed component groups | See `defaultSortComponents.json`. |
+| `--sortComponentsProps` | boolean | false | Alphabetize schema properties in components | Scope is `components.schemas.*.properties`. |
+| `sortSet.sortPathsBy` | enum | `original` | Path order mode | Values: `original`, `path`, `tags`. |
## Filtering
-| Key | Type | Semantics |
-|---|---|---|
-| `methods` | string[] | Remove matching HTTP methods from path items |
-| `inverseMethods` | string[] | Keep only matching methods |
-| `tags` | string[] | Remove operations/tag entries with matching tags |
-| `inverseTags` | string[] | Keep operations/tag entries with matching tags |
-| `operationIds` | string[] | Remove matching operationIds |
-| `inverseOperationIds` | string[] | Keep only matching operationIds |
-| `operations` | string[] | Remove matching `path#method` patterns |
-| `flags` | string[] | Remove objects containing matching marker keys |
-| `inverseFlags` | string[] | Keep objects containing matching marker keys |
-| `flagValues` | object[] | Remove objects/values matching key-value markers |
-| `inverseFlagValues` | object[] | Keep objects matching marker key-values |
-| `responseContent` | string[] | Remove matching response media types |
-| `inverseResponseContent` | string[] | Keep only matching response media types |
-| `requestContent` | string[] | Remove matching request media types |
-| `inverseRequestContent` | string[] | Keep only matching request media types |
-| `unusedComponents` | string[] | Remove unreferenced components recursively |
-| `stripFlags` | string[] | Remove flag keys after filtering |
-| `textReplace` | object[] | Replace text in `description`, `summary`, and `url` |
-| `preserveEmptyObjects` | boolean/string[] | Preserve selected empty objects instead of pruning |
+| Key | Type | Semantics |
+|--------------------------|------------------|-----------------------------------------------------|
+| `methods` | string[] | Remove matching HTTP methods from path items |
+| `inverseMethods` | string[] | Keep only matching methods |
+| `tags` | string[] | Remove operations/tag entries with matching tags |
+| `inverseTags` | string[] | Keep operations/tag entries with matching tags |
+| `operationIds` | string[] | Remove matching operationIds |
+| `inverseOperationIds` | string[] | Keep only matching operationIds |
+| `operations` | string[] | Remove matching `path#method` patterns |
+| `flags` | string[] | Remove objects containing matching marker keys |
+| `inverseFlags` | string[] | Keep objects containing matching marker keys |
+| `flagValues` | object[] | Remove objects/values matching key-value markers |
+| `inverseFlagValues` | object[] | Keep objects matching marker key-values |
+| `responseContent` | string[] | Remove matching response media types |
+| `inverseResponseContent` | string[] | Keep only matching response media types |
+| `requestContent` | string[] | Remove matching request media types |
+| `inverseRequestContent` | string[] | Keep only matching request media types |
+| `unusedComponents` | string[] | Remove unreferenced components recursively |
+| `stripFlags` | string[] | Remove flag keys after filtering |
+| `textReplace` | object[] | Replace text in `description`, `summary`, and `url` |
+| `preserveEmptyObjects` | boolean/string[] | Preserve selected empty objects instead of pruning |
Important behavior:
- Filter stage is recursive and may run multiple passes to remove now-unused components.
@@ -71,30 +71,38 @@ Important behavior:
## Casing
-| Option (`casingSet`) | Effect |
-|---|---|
-| `operationId` | Casing for operation IDs |
-| `properties` | Casing for schema/path property keys and required names |
-| `parametersQuery` / `parametersHeader` / `parametersPath` / `parametersCookie` | Casing for inline parameter names by location |
-| `componentsSchemas` / `componentsExamples` / `componentsHeaders` / `componentsResponses` / `componentsRequestBodies` / `componentsSecuritySchemes` | Casing for component keys |
-| `componentsParametersQuery` / `componentsParametersHeader` / `componentsParametersPath` / `componentsParametersCookie` | Casing for `components.parameters` keys by parameter `in` |
+| Option (`casingSet`) | Effect |
+|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------|
+| `operationId` | Casing for operation IDs |
+| `operationIdKeepChars` | Extra characters to preserve while casing `operationId` |
+| `summary` / `description` | Casing for operation summaries and descriptions |
+| `summaryKeepChars` / `descriptionKeepChars` | Extra characters to preserve while casing summaries and descriptions |
+| `properties` | Casing for schema/path property keys and required names |
+| `propertiesKeepChars` | Extra characters to preserve while casing properties |
+| `parametersQuery` / `parametersHeader` / `parametersPath` / `parametersCookie` | Casing for inline parameter names by location |
+| `parametersQueryKeepChars` / `parametersHeaderKeepChars` / `parametersPathKeepChars` / `parametersCookieKeepChars` | Extra characters to preserve while casing inline parameter names |
+| `componentsSchemas` / `componentsExamples` / `componentsHeaders` / `componentsResponses` / `componentsRequestBodies` / `componentsSecuritySchemes` | Casing for component keys |
+| `componentsSchemasKeepChars` / `componentsExamplesKeepChars` / `componentsHeadersKeepChars` / `componentsResponsesKeepChars` / `componentsRequestBodiesKeepChars` / `componentsSecuritySchemesKeepChars` | Extra characters to preserve while casing component keys |
+| `componentsParametersQuery` / `componentsParametersHeader` / `componentsParametersPath` / `componentsParametersCookie` | Casing for `components.parameters` keys by parameter `in` |
+| `componentsParametersQueryKeepChars` / `componentsParametersHeaderKeepChars` / `componentsParametersPathKeepChars` / `componentsParametersCookieKeepChars` | Extra characters to preserve while casing `components.parameters` keys |
Reference behavior:
- Related `$ref` values are updated for renamed component keys.
+- Custom keep chars are merged with the built-in defaults (`$`, `@`).
## Generate
-| Key (`generateSet`) | Type | Default | Effect |
-|---|---|---|---|
-| `operationIdTemplate` | string | none | Template used to generate operationIds |
-| `overwriteExisting` | boolean | false | If true, regenerate even existing operationIds |
+| Key (`generateSet`) | Type | Default | Effect |
+|-----------------------|---------|---------|------------------------------------------------|
+| `operationIdTemplate` | string | none | Template used to generate operationIds |
+| `overwriteExisting` | boolean | false | If true, regenerate even existing operationIds |
Template resolves against operation context using internal parse template utilities.
## Overlay
-| Option | Type | Effect |
-|---|---|---|
+| Option | Type | Effect |
+|-----------------------|------|--------------------------------------------------|
| `--overlayFile`, `-l` | path | Load overlay actions and apply to input document |
Notable behavior:
@@ -104,10 +112,10 @@ Notable behavior:
## Convert and rename
-| Option | Type | Effect |
-|---|---|---|
+| Option | Type | Effect |
+|---------------|--------|-------------------------------|
| `--convertTo` | string | Convert OpenAPI to 3.1 or 3.2 |
-| `--rename` | string | Replace `info.title` |
+| `--rename` | string | Replace `info.title` |
Convert stage details:
- Converts nullable/exclusive limits/example/const forms as needed.
@@ -116,10 +124,10 @@ Convert stage details:
## Split and bundle
-| Option | Type | Default | Effect |
-|---|---|---|---|
-| `--split` | boolean | false | Write multi-file output structure |
-| `--no-bundle` | boolean | false | Disable local/remote `$ref` bundling during parse |
+| Option | Type | Default | Effect |
+|---------------|---------|---------|---------------------------------------------------|
+| `--split` | boolean | false | Write multi-file output structure |
+| `--no-bundle` | boolean | false | Disable local/remote `$ref` bundling during parse |
Split behavior:
- Requires `--output`.
diff --git a/test/casing.test.js b/test/casing.test.js
index d61ecae..9587c72 100644
--- a/test/casing.test.js
+++ b/test/casing.test.js
@@ -165,8 +165,17 @@ describe('openapi-format CLI casing tests', () => {
it('casing should keep default @$ characters', async () => {
expect(of.changeCase('@openapi-format$}}', 'snake_case')).toBe('@openapi_format$');
});
- it('casing should remove all custom characters', async () => {
+ it('casing should keep custom dot characters and defaults', async () => {
+ expect(of.changeCase('cursor.created_at', 'camelCase', ['.'])).toBe('cursor.createdAt');
+ });
+ it('casing should merge custom characters with built-in defaults', async () => {
+ expect(of.changeCase('@openapi-$format.v2', 'camelCase', ['.'])).toBe('@openapi$format.v2');
+ });
+ it('casing should allow callers to disable built-in defaults with an empty array', async () => {
expect(of.changeCase('@openapi-$format}}', 'snake_case', [])).toBe('openapi_format');
});
+ it('casing should keep default @$ characters when no custom keep chars are provided', async () => {
+ expect(of.changeCase('@openapi-$format}}', 'snake_case')).toBe('@openapi$format');
+ });
});
});
diff --git a/test/openapi-core.test.js b/test/openapi-core.test.js
index e842441..529b29b 100644
--- a/test/openapi-core.test.js
+++ b/test/openapi-core.test.js
@@ -123,6 +123,160 @@ describe('openapi-format core API', () => {
expect(result.data.paths['/pets'].get.summary).toBe('list-pets');
expect(result.data.paths['/pets'].get.description).toBe('returns-all-pets');
});
+
+ it('openapiChangeCase should keep configured separator characters for parameter names', async () => {
+ const doc = {
+ openapi: '3.0.0',
+ info: {title: 'API', version: '1.0.0'},
+ paths: {
+ '/pets': {
+ get: {
+ parameters: [
+ {
+ name: 'cursor.created_at',
+ in: 'query',
+ schema: {type: 'string'}
+ }
+ ],
+ responses: {200: {description: 'ok'}}
+ }
+ }
+ }
+ };
+
+ const result = await openapiChangeCase(doc, {
+ casingSet: {
+ parametersQuery: 'camelCase',
+ parametersQueryKeepChars: ['.']
+ }
+ });
+
+ expect(result.data.paths['/pets'].get.parameters[0].name).toBe('cursor.createdAt');
+ });
+
+ it('openapiChangeCase should keep configured separator characters for component schema keys and refs', async () => {
+ const doc = {
+ openapi: '3.0.0',
+ info: {title: 'API', version: '1.0.0'},
+ paths: {
+ '/pets': {
+ get: {
+ responses: {
+ 200: {
+ description: 'ok',
+ content: {'application/json': {schema: {$ref: '#/components/schemas/Pet.v1'}}}
+ }
+ }
+ }
+ }
+ },
+ components: {
+ schemas: {
+ 'Pet.v1': {type: 'object'}
+ }
+ }
+ };
+
+ const result = await openapiChangeCase(doc, {
+ casingSet: {
+ componentsSchemas: 'camelCase',
+ componentsSchemasKeepChars: ['.']
+ }
+ });
+
+ expect(Object.keys(result.data.components.schemas)).toContain('pet.v1');
+ expect(result.data.paths['/pets'].get.responses[200].content['application/json'].schema.$ref).toBe(
+ '#/components/schemas/pet.v1'
+ );
+ });
+
+ it('openapiChangeCase should keep configured separator characters for component parameter keys and refs', async () => {
+ const doc = {
+ openapi: '3.0.0',
+ info: {title: 'API', version: '1.0.0'},
+ paths: {
+ '/pets': {
+ get: {
+ parameters: [
+ {
+ $ref: '#/components/parameters/Cursor.v1'
+ }
+ ],
+ responses: {200: {description: 'ok'}}
+ }
+ }
+ },
+ components: {
+ parameters: {
+ 'Cursor.v1': {
+ name: 'cursor.v1',
+ in: 'query',
+ schema: {type: 'string'}
+ }
+ }
+ }
+ };
+
+ const result = await openapiChangeCase(doc, {
+ casingSet: {
+ componentsParametersQuery: 'camelCase',
+ componentsParametersQueryKeepChars: ['.']
+ }
+ });
+
+ expect(Object.keys(result.data.components.parameters)).toContain('cursor.v1');
+ expect(result.data.components.parameters['cursor.v1'].name).toBe('cursor.v1');
+ expect(result.data.paths['/pets'].get.parameters[0].$ref).toBe('#/components/parameters/cursor.v1');
+ });
+
+ it('openapiChangeCase should leave unconfigured component parameter keys unchanged', async () => {
+ const doc = {
+ openapi: '3.0.0',
+ info: {title: 'API', version: '1.0.0'},
+ paths: {
+ '/pets': {
+ get: {
+ parameters: [
+ {
+ $ref: '#/components/parameters/Cursor.v1'
+ },
+ {
+ $ref: '#/components/parameters/X-Request-Id'
+ }
+ ],
+ responses: {200: {description: 'ok'}}
+ }
+ }
+ },
+ components: {
+ parameters: {
+ 'Cursor.v1': {
+ name: 'cursor.v1',
+ in: 'query',
+ schema: {type: 'string'}
+ },
+ 'X-Request-Id': {
+ name: 'X-Request-Id',
+ in: 'header',
+ schema: {type: 'string'}
+ }
+ }
+ }
+ };
+
+ const result = await openapiChangeCase(doc, {
+ casingSet: {
+ componentsParametersQuery: 'camelCase',
+ componentsParametersQueryKeepChars: ['.']
+ }
+ });
+
+ expect(Object.keys(result.data.components.parameters)).toContain('cursor.v1');
+ expect(Object.keys(result.data.components.parameters)).toContain('X-Request-Id');
+
+ expect(result.data.paths['/pets'].get.parameters[0].$ref).toBe('#/components/parameters/cursor.v1');
+ expect(result.data.paths['/pets'].get.parameters[1].$ref).toBe('#/components/parameters/X-Request-Id');
+ });
});
describe('openapiSplit API', () => {
diff --git a/test/yaml-casing-parameters/customCasing.yaml b/test/yaml-casing-parameters/customCasing.yaml
index 4c86767..5a94116 100644
--- a/test/yaml-casing-parameters/customCasing.yaml
+++ b/test/yaml-casing-parameters/customCasing.yaml
@@ -1,3 +1,5 @@
-parametersQuery: snake_case
+parametersQuery: camelCase
+parametersQueryKeepChars:
+ - .
parametersHeader: snake_case
parametersPath: snake_case
diff --git a/test/yaml-casing-parameters/input.yaml b/test/yaml-casing-parameters/input.yaml
index 8fd9cc0..d5651ea 100644
--- a/test/yaml-casing-parameters/input.yaml
+++ b/test/yaml-casing-parameters/input.yaml
@@ -121,7 +121,7 @@ paths:
schema:
type: integer
format: int64
- - name: limitItems
+ - name: cursor.created_at
in: query
description: ID of pet to return
required: true
diff --git a/test/yaml-casing-parameters/output.yaml b/test/yaml-casing-parameters/output.yaml
index 14a8e59..9b9e1e8 100644
--- a/test/yaml-casing-parameters/output.yaml
+++ b/test/yaml-casing-parameters/output.yaml
@@ -121,7 +121,7 @@ paths:
schema:
type: integer
format: int64
- - name: limit_items
+ - name: cursor.createdAt
in: query
description: ID of pet to return
required: true
@@ -604,7 +604,7 @@ components:
summary: A payload example for a notification
parameters:
skipParam:
- name: skip_items
+ name: skipItems
in: query
description: number of items to skip
required: true
@@ -612,7 +612,7 @@ components:
type: integer
format: int32
limitParam:
- name: $limit_param
+ name: $limitParam
in: query
description: max records to return
required: true
diff --git a/types/openapi-format.d.ts b/types/openapi-format.d.ts
index 71ccc07..29ff985 100644
--- a/types/openapi-format.d.ts
+++ b/types/openapi-format.d.ts
@@ -69,19 +69,41 @@ declare module 'openapi-format' {
export interface OpenAPICasingSet {
operationId?: string;
+ operationIdKeepChars?: string[];
+ summary?: string;
+ summaryKeepChars?: string[];
+ description?: string;
+ descriptionKeepChars?: string[];
properties?: string;
+ propertiesKeepChars?: string[];
parametersQuery?: string;
+ parametersQueryKeepChars?: string[];
parametersHeader?: string;
+ parametersHeaderKeepChars?: string[];
parametersPath?: string;
+ parametersPathKeepChars?: string[];
+ parametersCookie?: string;
+ parametersCookieKeepChars?: string[];
componentsExamples?: string;
+ componentsExamplesKeepChars?: string[];
componentsSchemas?: string;
+ componentsSchemasKeepChars?: string[];
componentsHeaders?: string;
+ componentsHeadersKeepChars?: string[];
componentsResponses?: string;
+ componentsResponsesKeepChars?: string[];
componentsRequestBodies?: string;
+ componentsRequestBodiesKeepChars?: string[];
componentsSecuritySchemes?: string;
+ componentsSecuritySchemesKeepChars?: string[];
componentsParametersQuery?: string;
+ componentsParametersQueryKeepChars?: string[];
componentsParametersHeader?: string;
+ componentsParametersHeaderKeepChars?: string[];
componentsParametersPath?: string;
+ componentsParametersPathKeepChars?: string[];
+ componentsParametersCookie?: string;
+ componentsParametersCookieKeepChars?: string[];
[key: string]: unknown;
}
@@ -199,7 +221,7 @@ declare module 'openapi-format' {
export function detectFormat(input: string): Promise<'json' | 'yaml' | 'unknown'>;
export function analyzeOpenApi(oaObj: OpenApiDocument): AnalyzeOpenApiResult;
- export function changeCase(valueAsString: string, caseType: string): string;
+ export function changeCase(valueAsString: string, caseType: string, customKeepChars?: string[]): string;
export function resolveJsonPath(obj: Record | unknown[], path: string): JsonPathNode[];
export function resolveJsonPathValue(obj: Record | unknown[], path: string): unknown[];
}
diff --git a/utils/casing.js b/utils/casing.js
index 69200b3..4d26287 100644
--- a/utils/casing.js
+++ b/utils/casing.js
@@ -46,14 +46,15 @@ function changeParametersCasingEnabled(casingSet) {
* Change Object keys case function
* @param {object} obj
* @param {string} caseType
+ * @param {string[]} customKeepChars Custom characters to keep
* @returns {*}
*/
-function changeObjKeysCase(obj, caseType) {
+function changeObjKeysCase(obj, caseType, customKeepChars = null) {
if (!isObject(obj)) return obj;
const orgObj = JSON.parse(JSON.stringify(obj)); // Deep copy of the object
let replacedItems = Object.keys(orgObj).map(key => {
- const newKey = changeCase(key, caseType);
+ const newKey = changeCase(key, caseType, customKeepChars);
return {[newKey]: orgObj[key]};
});
return Object.assign({}, ...replacedItems);
@@ -63,18 +64,47 @@ function changeObjKeysCase(obj, caseType) {
* Change object keys case in array function
* @param {object} node
* @param {string} caseType
+ * @param {string[]} customKeepChars Custom characters to keep
* @returns {*}
*/
-function changeArrayObjKeysCase(node, caseType) {
+function changeArrayObjKeysCase(node, caseType, customKeepChars = null) {
if (!isArray(node)) return node;
const casedNode = JSON.parse(JSON.stringify(node)); // Deep copy of the schema object
for (let i = 0; i < casedNode.length; i++) {
- casedNode[i] = changeObjKeysCase(casedNode[i], caseType);
+ casedNode[i] = changeObjKeysCase(casedNode[i], caseType, customKeepChars);
}
return casedNode;
}
+/**
+ * Build the keep character list for case-anything.
+ * Always preserves the built-in defaults and appends any custom characters.
+ * @param {string[]} customKeepChars Custom characters to keep
+ * @returns {string[]}
+ */
+function normalizeKeepChars(customKeepChars = null) {
+ if (customKeepChars == null) {
+ return ['$', '@'];
+ }
+
+ if (!isArray(customKeepChars)) {
+ return customKeepChars;
+ }
+
+ if (customKeepChars.length === 0) {
+ return [];
+ }
+
+ const keepChars = ['$', '@'];
+ customKeepChars.forEach(char => {
+ if (!keepChars.includes(char)) {
+ keepChars.push(char);
+ }
+ });
+ return keepChars;
+}
+
/**
* Change case function
* @param {string} valueAsString The value to change
@@ -84,7 +114,7 @@ function changeArrayObjKeysCase(node, caseType) {
*/
function changeCase(valueAsString, caseType, customKeepChars = null) {
if (!isString(valueAsString) || valueAsString === '') return valueAsString;
- const keepChars = customKeepChars || ['$', '@'];
+ const keepChars = normalizeKeepChars(customKeepChars);
const cleanedString = valueAsString.replace(/\[(.*?)]/g, (match, p1) => {
return ' ' + p1.replace(/([A-Z])/g, ' $1') + ' ';
});