diff --git a/CHANGELOG.md b/CHANGELOG.md index e1cafd9..7a70830 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/test/util-file.test.js b/test/util-file.test.js index 37cfd84..e66a04e 100644 --- a/test/util-file.test.js +++ b/test/util-file.test.js @@ -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'}; @@ -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); diff --git a/test/yaml-big-numbers-scalar/input.yaml b/test/yaml-big-numbers-scalar/input.yaml new file mode 100644 index 0000000..ce59ac0 --- /dev/null +++ b/test/yaml-big-numbers-scalar/input.yaml @@ -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 diff --git a/test/yaml-big-numbers-scalar/options.yaml b/test/yaml-big-numbers-scalar/options.yaml new file mode 100644 index 0000000..a134fd1 --- /dev/null +++ b/test/yaml-big-numbers-scalar/options.yaml @@ -0,0 +1,3 @@ +verbose: true +output: output.yaml +no-sort: false diff --git a/test/yaml-big-numbers-scalar/output.yaml b/test/yaml-big-numbers-scalar/output.yaml new file mode 100644 index 0000000..23b7253 --- /dev/null +++ b/test/yaml-big-numbers-scalar/output.yaml @@ -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 diff --git a/utils/file.js b/utils/file.js index 59a8e84..3df9bd6 100644 --- a/utils/file.js +++ b/utils/file.js @@ -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') { @@ -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; } @@ -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); } @@ -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.