Quick answer: Run five YAML-to-JSON conversion tests: verify scalar types, reject ambiguous mapping keys, define the document-stream shape, control alias behavior, and check the final serialized JSON. Parsing success is only the first boundary. The converted result must also preserve the values and structure that the consuming application expects.
A configuration conversion finishes without an error, but a feature flag changes type or an identifier loses a digit. The JSON looks tidy. The application now receives a different contract.
For the outbound direction, the JSON-to-YAML deployment checklist checks emitted text and parsed values before the receiving configuration loader applies them.
These tests complement the JSON versus YAML comparison. Examples were executed with JS-YAML 5.4.3 and explicit schema options; they are not universal promises about every YAML parser. The project's 2026 changelog records scalar-resolution and mapping changes in version 5, reinforcing why parser upgrades deserve conversion fixtures rather than a syntax-only smoke test.
1. Do plain scalars become the exact JSON types you expect?
Parse a fixture containing booleans, nulls, numeric-looking identifiers, dates, and quoted equivalents, then assert both each value and its runtime type. Pin the parser schema explicitly. A plain scalar's appearance does not establish its meaning across YAML versions or libraries, and quoting can be necessary to preserve an intended string.
Start with values that matter to the application, not an exhaustive language demonstration. Include a country-like label, a port-like identifier with leading zeros, and an empty setting. For each, write whether the application expects text, a boolean, a number, or a missing value. Then compare that expectation with what the converter actually produces.
The YAML specification's change notes explain important differences from older YAML conventions. Library choices matter too: the separate yaml package's schema documentation shows how schema selection affects parsing. Do not transfer an option name or observed result from one package to another without testing it.
import assert from 'node:assert/strict';
import * as yaml from 'js-yaml';
const parsed = yaml.load([
'enabled: true',
'label: yes',
'reference: 0123',
'release: 2026-10-18',
'empty:',
'absent: null',
'literal: "true"'
].join('\n'), { schema: yaml.JSON_SCHEMA });
assert.equal(parsed.enabled, true);
assert.equal(parsed.label, 'yes');
assert.equal(parsed.reference, '0123');
assert.equal(parsed.release, '2026-10-18');
assert.equal(parsed.empty, '');
assert.equal(parsed.absent, null);
assert.equal(parsed.literal, 'true');
This fixture intentionally captures the selected JS-YAML configuration, including its empty-value behavior. If your deployment uses another loader, run the same source through that loader and compare results. A difference should trigger a contract decision, not a silent normalization step.
Store the fixture source together with the expected JSON values and parser options. During an upgrade, run the old and new configurations against that corpus and review every changed result. Do not accept a green test that only checks whether both versions produced an object. The important assertions concern the individual field types and values that drive application decisions, including values that are intentionally rejected.
The YAML-to-JSON converter uses JS-YAML's JSON schema and documents its scalar choices. Use a sanitized fixture to inspect the result, but do not assume its settings match a deployment tool's loader. For fields whose absence and explicit null mean different things, pair this test with the JSON null and missing-value guide.
2. Are duplicate or non-string mapping keys rejected deliberately?
Test duplicate field names and mappings whose keys are not ordinary strings before relying on a JSON object result. Reject ambiguity instead of silently keeping one value or stringifying complex keys. If preserving key identity is essential, choose a different representation and verify it before the parser has discarded information.
A duplicate timeout field is not a harmless formatting detail. If one component keeps the first value and another keeps the last, the reviewed configuration can differ from the deployed one. The conversion boundary is a useful place to fail clearly, before downstream validation sees an already simplified object.
The YAML 1.2.2 specification describes mappings with unique keys and a richer representation model than JSON objects. JavaScript object property names cannot directly preserve every YAML key type. For a JSON-facing configuration contract, require ordinary string keys or specify a lossless alternate encoding rather than accepting whatever a convenience loader returns.
import assert from 'node:assert/strict';
import * as yaml from 'js-yaml';
const options = { schema: yaml.JSON_SCHEMA };
assert.throws(() => yaml.load('timeout: 10\ntimeout: 20', options),
/duplicated mapping key/);
assert.throws(() => yaml.load('? [region, zone]\n: value', options),
/complex keys/);
assert.deepEqual(yaml.load('"region-zone": value', options),
{ 'region-zone': 'value' });
The error assertions reflect the tested package version. Production interfaces should use stable application-owned error categories rather than depending on exact wording forever. Keep the source location in safe diagnostics so someone can correct the ambiguous configuration quickly.
Also consider numeric keys that become object property strings. Once that conversion has happened, inspecting Object.keys() cannot reconstruct the original scalar type. If your policy forbids such keys, enforce it using a suitable parser representation or an earlier validation stage. A post-conversion string-key check alone cannot prove that the source used string keys.
Keep duplicate-key rejection separate from configuration-layer merging. Two source documents might intentionally override one another under a deployment policy, but repeated keys within a single mapping are a different event. Test both paths so a convenient last-value-wins setting does not quietly become the behavior for ambiguous source files. Record the source layer of an override without retaining secrets in diagnostic logs.
3. Does the consumer agree with the number of YAML documents?
Test empty input, one document, and a multi-document stream, then assert the exact JSON root shape your consumer accepts. Choose rejection or a documented array representation when several documents are present. Do not silently merge them, take the first document, or wrap a single document differently without an explicit application contract.
A deployment manifest may legitimately contain several resources separated by document markers. A service configuration might require one object. The same converter behavior can therefore be convenient for one workflow and incorrect for another. Decide the root contract at the application boundary rather than treating the file extension as sufficient information.
The JS-YAML documentation exposes separate single-document and stream-loading APIs. A wrapper must still decide what to return after loading. The following function mirrors a simple documented conversion policy, not an application-specific merge operation:
import assert from 'node:assert/strict';
import * as yaml from 'js-yaml';
function convert(source) {
const documents = yaml.loadAll(source, null, { schema: yaml.JSON_SCHEMA });
return documents.length === 1 ? documents[0] : documents;
}
assert.deepEqual(convert('name: api'), { name: 'api' });
assert.deepEqual(convert('name: api\n---\nname: worker'),
[{ name: 'api' }, { name: 'worker' }]);
assert.deepEqual(convert(''), []);
assert.equal(convert('null'), null);
For an application that requires exactly one configuration object, add a document-count assertion before returning a value. Validate the root type separately: a single document can still be a scalar, array, or null. Counting documents does not establish that the resulting value has the required fields.
Test the actual import adapter too. A web endpoint might accept the array successfully while a later persistence function expects an object and saves nothing useful. Assert the stored configuration or generated deployment artifact, not just a successful converter response. Keep the multi-document policy visible in user-facing import instructions so users know whether to split a file themselves.
Include a document with an empty root and a document containing an explicit null in the acceptance corpus. They may be equally unacceptable to the application while still arriving through different parser paths. Check that neither case triggers a fallback to an unrelated default configuration or deletes the currently active settings. A rejected import should leave the last approved artifact available for a controlled retry.
4. Can aliases create unsupported graphs or excessive expansion?
Test a small shared alias, an unsupported recursive alias, and the loader's configured alias limit. JSON output repeats values rather than preserving YAML anchor notation, and ordinary serialization cannot encode circular runtime identity. Apply resource controls and a clear rejection policy before treating any successfully parsed graph as a safe exportable tree.
For background, YAML supports anchors and aliases as part of its serialization model. JSON does not have an equivalent native reference token. Distinguish repeated content from cycles: two references to a small shared mapping can serialize successfully, while a mapping that refers back to itself cannot use ordinary JSON serialization unchanged.
import assert from 'node:assert/strict';
import * as yaml from 'js-yaml';
const source = 'first: &shared {port: 8080}\nsecond: *shared';
const parsed = yaml.load(source, { schema: yaml.JSON_SCHEMA });
assert.equal(parsed.first, parsed.second);
const output = JSON.parse(JSON.stringify(parsed));
assert.deepEqual(output.first, output.second);
assert.notEqual(output.first, output.second);
assert.throws(() => yaml.load(source,
{ schema: yaml.JSON_SCHEMA, maxAliases: 0 }), /maxAliases/);
const cycle = yaml.load('node: &n {child: *n}', { schema: yaml.JSON_SCHEMA });
assert.throws(() => JSON.stringify(cycle), TypeError);
These fixtures are intentionally tiny. They verify identity, serialization, and an available loader option without creating a large expansion workload. An alias-count limit is not a complete memory or output-size budget: also bound source size, nesting, conversion time, and generated output according to your application's needs.
Test merge-key assumptions separately when migrating existing configuration. Under the converter's selected schema, a << key is not an instruction to apply every legacy merge convention. Do not replace it with a homegrown merge operation without defining precedence and allowed fields. The JSON configuration review is useful for the application-level decisions after conversion.
When conversion fails, do not return a partially serialized document as if the operation succeeded. Preserve the original source for the user to correct, report an appropriate category, and leave the previous valid configuration unchanged. Treat the import as one staged operation rather than saving individual fields while conversion is still underway.
5. Does the final JSON preserve the values the application will use?
Serialize and parse the converted result, then validate the resulting JSON against the application contract. Check unsafe integers and values admitted by broader YAML schemas that JSON cannot represent directly. A final JSON parse proves syntax, not semantic fidelity, and precision can already be lost before serialization begins.
MDN's JSON.stringify reference documents behavior for non-finite numbers and circular structures. If another YAML schema produces a runtime infinity, ordinary serialization can turn it into JSON null rather than preserving the original meaning. The JSON-oriented schema used above avoids that particular implicit conversion by retaining the corresponding plain token as text.
import assert from 'node:assert/strict';
import * as yaml from 'js-yaml';
const broad = yaml.load('limit: .inf', { schema: yaml.CORE_SCHEMA });
assert.equal(broad.limit, Infinity);
assert.equal(JSON.stringify(broad), '{"limit":null}');
const explicit = yaml.load('limit: .inf', { schema: yaml.JSON_SCHEMA });
assert.equal(explicit.limit, '.inf');
const numeric = yaml.load('id: 9007199254740993', { schema: yaml.JSON_SCHEMA });
assert.equal(Number.isSafeInteger(numeric.id), false);
const quoted = yaml.load('id: "9007199254740993"', { schema: yaml.JSON_SCHEMA });
assert.equal(JSON.parse(JSON.stringify(quoted)).id, '9007199254740993');
Use the large-number precision guide when an identifier must survive JavaScript parsing exactly. Checking for an unsafe integer can reject suspicious input, but it cannot restore digits that were already rounded. Preserve such identifiers as strings from the start or use an explicitly designed precision-preserving pipeline.
Run the application's real schema validation after conversion and before persistence. A date remaining a string is correct for conversion but does not prove it is an acceptable date for the business operation. Similarly, retaining an unsupported scalar as text does not make it a valid numeric setting. The JSON Schema validation checks cover this next boundary.
Add a multiline-text fixture when configuration contains templates, certificates, or scripts. Compare the parsed string including its final newline, not only a trimmed preview. Trimming inside a test can hide a conversion change that affects the downstream consumer. Keep presentation choices such as indentation separate from value assertions, and verify that copying or downloading the final JSON does not apply another unintended escape or normalization layer.
All five JavaScript examples were executed locally with the stated parser configuration. No cross-parser compatibility or adversarial resource-exhaustion result is claimed. Keep these fixtures with the converter, rerun them when the dependency or options change, and compare the final application artifact before approving the migration.