diff --git a/CHANGELOG.md b/CHANGELOG.md
index c798a79..c52f7e4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
## unreleased
- CLI: Fix YAML output to preserve x-version number formatting
+- Casing - Configure characters to keep
## [1.31.0] - 2026-04-12
diff --git a/openapi-format.js b/openapi-format.js
index 84101fa..31faf66 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 (
@@ -945,7 +946,7 @@ async function openapiChangeCase(oaObj, options) {
const changeCasingKeyPlan = changeCasingKeyPlans[parameterFoundIn];
if (changeCasingKeyPlan) {
// 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]};
}
@@ -957,9 +958,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 +968,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 +993,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 +1025,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 +1081,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 +1098,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 +1116,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..57901e4 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,14 +1349,20 @@ 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.
+- `operationIdKeepChars`: Keeps selected characters while casing `operationId`. Example: `.` for dotted identifiers.
- `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.
+- `propertiesKeepChars`: Keeps selected characters while casing properties. Example: `.`.
+- `parametersQuery`, `parametersHeader`, `parametersPath`, `parametersCookie`: Define different casing styles for parameters based on their location (query, header, path, cookie). Example: snake_case, kebab-case.
+- `parametersQueryKeepChars`, `parametersHeaderKeepChars`, `parametersPathKeepChars`, `parametersCookieKeepChars`: Keep selected characters while casing inline parameters.
+- `componentsParametersQueryKeepChars`, `componentsParametersHeaderKeepChars`, `componentsParametersPathKeepChars`, `componentsParametersCookieKeepChars`: Keep selected characters while casing referenced parameters in `components.parameters`.
- and many more
See [OpenAPI formatting configuration options](#openapi-formatting-configuration-options) for the full list of casing options
@@ -1713,4 +1725,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..27f7bd5 100644
--- a/test/openapi-core.test.js
+++ b/test/openapi-core.test.js
@@ -123,6 +123,111 @@ 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');
+ });
});
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') + ' ';
});