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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
## unreleased

- CLI: Prevent YAML data loss when quoted scalars contain high-precision numbers (#233)

## [1.33.5] - 2026-06-23

- CLI: Fix YAML output to keep quotes for $ref values (#230)
Expand Down
37 changes: 37 additions & 0 deletions test/util-file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,36 @@ describe('openapi-format CLI file tests', () => {
expect(result).toEqual({name: 'John', age: 30});
});

it('should preserve quoted JSON examples with high-precision numbers and following schemas', async () => {
const yamlString = `openapi: 3.1.0
info:
title: openapi-format schema-drop reproduction
version: 1.0.0
paths: {}
components:
schemas:
Before:
type: object
description: This schema contains an escaped JSON example with a high-precision number.
example: "{\\n \\"score\\": 0.8189693396524255,\\n}"
CreateResponse:
type: object
description: This schema should remain in the formatted document.
properties:
id:
type: string
`;

const result = await parseString(yamlString, {format: 'yaml'});

expect(result).not.toBeInstanceOf(Error);
expect(Object.keys(result.components.schemas)).toEqual(['Before', 'CreateResponse']);
expect(result.components.schemas.Before.example).toBe(
'{\n "score": 0.8189693396524255,\n}'
);
expect(result.components.schemas.CreateResponse.properties.id.type).toBe('string');
});

it('should detect dominant double quotes from YAML input', async () => {
const yamlString = 'name: "John"\ncity: "London"\ncountry: \'UK\'\n';
const options = {yamlQuoteStyle: 'detect'};
Expand Down Expand Up @@ -449,6 +479,13 @@ describe('openapi-format CLI file tests', () => {
expect(result).toBeInstanceOf(SyntaxError);
});

it('should return a YAML parsing error instead of recovering a partial document', async () => {
const invalidString = 'foo: "unterminated\nbar: value\n';
const result = await parseString(invalidString, {format: 'yaml'});

expect(result).toBeInstanceOf(Error);
});

it('should quote unquoted $ref value starting with #', async () => {
const yamlString = 'schema:\n $ref: #/components/schemas/Example';
const result = await parseString(yamlString);
Expand Down
17 changes: 17 additions & 0 deletions test/yaml-big-numbers-scalar/input.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
openapi: 3.1.0
info:
title: openapi-format schema-drop reproduction
version: 1.0.0
paths: {}
components:
schemas:
Before:
type: object
description: This schema contains an escaped JSON example with a high-precision number.
example: "{\n \"score\": 0.8189693396524255,\n}"
CreateResponse:
type: object
description: This schema should remain in the formatted document.
properties:
id:
type: string
3 changes: 3 additions & 0 deletions test/yaml-big-numbers-scalar/options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
verbose: true
output: output.yaml
no-sort: false
20 changes: 20 additions & 0 deletions test/yaml-big-numbers-scalar/output.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
openapi: 3.1.0
info:
title: openapi-format schema-drop reproduction
version: 1.0.0
paths: {}
components:
schemas:
Before:
description: This schema contains an escaped JSON example with a high-precision number.
type: object
example: |-
{
"score": 0.8189693396524255,
}
CreateResponse:
description: This schema should remain in the formatted document.
type: object
properties:
id:
type: string
46 changes: 37 additions & 9 deletions utils/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,19 @@ async function parseString(str, options = {}) {
return str;
}

// Convert large number values safely before parsing
let encodedContent = encodeLargeNumbers(str);
encodedContent = addQuotesToRefInString(encodedContent);

// Default to YAML format unless specified as JSON
const toYaml = options.format !== 'json' && (!options.hasOwnProperty('json') || options.json !== true);

if (toYaml) {
try {
const doc = yaml.parseDocument(encodedContent);
// Parse YAML before encoding large numbers so quoted strings remain untouched.
const doc = yaml.parseDocument(addQuotesToRefInString(str));
if (doc.errors.length > 0) {
return new SyntaxError(doc.errors[0].message);
}

// Convert large number scalar values safely after parsing, without touching strings.
encodeLargeNumberScalars(doc);
applyYamlParseMetadata(doc, options);
const obj = doc.toJS();
if (typeof obj === 'object') {
Expand All @@ -240,8 +243,8 @@ async function parseString(str, options = {}) {
}
} else {
try {
// Try parsing as JSON
return JSON.parse(encodedContent);
// Encode large JSON numbers before parsing so their precision is preserved.
return JSON.parse(encodeLargeNumbers(str));
} catch (jsonError) {
return jsonError;
}
Expand Down Expand Up @@ -330,8 +333,7 @@ async function parseFile(filePath, options = {}) {
let rawContent = await readFile(filePath, options);

if (options.format === 'yaml') {
const encodedContent = addQuotesToRefInString(encodeLargeNumbers(rawContent));
const doc = yaml.parseDocument(encodedContent);
const doc = yaml.parseDocument(addQuotesToRefInString(rawContent));
applyYamlParseMetadata(doc, options);
}

Expand Down Expand Up @@ -516,6 +518,32 @@ function encodeLargeNumbers(inputContent) {
});
}

/**
* Convert large number scalar values safely after YAML parsing.
* @param {import('yaml').Document} doc
*/
function encodeLargeNumberScalars(doc) {
yaml.visit(doc, {
Pair(_, pair) {
const value = pair.value;
if (
!yaml.isScalar(value) ||
typeof value.value !== 'number' ||
typeof value.source !== 'string'
) {
return;
}

const source = value.source;
const digitCount = source.replace(/[^0-9]/g, '').length;
if (Number(source).toString().includes('e') || digitCount > 15) {
value.value = `${source}===`;
value.type = 'QUOTE_DOUBLE';
}
}
});
}

/**
* Extract YAML scalar formatting metadata that needs to be preserved when
* writing output.
Expand Down
Loading