INI to JSON: Turning Sections, Keys and Values into Clean Objects
A practical guide to converting INI config to JSON: how sections become nested objects, how key=value lines map to properties, type coercion, and migrating legacy config.
INI to JSON: Turning Sections, Keys and Values into Clean Objects
Every long-lived service eventually collects an INI file. It started as a tidy [server] block with a host and a port, and a few years later it carries database credentials, feature flags, a [logging] section, and a graveyard of commented-out options nobody remembers writing. Then you rewrite the service in something that reads JSON, and you have to move all of that across without losing a single key. This post walks through exactly how INI maps onto JSON, where the surprises hide, and how to do the migration without retyping anything.
The core mapping: sections become objects
The whole conversion rests on three rules, and once they click the rest is mechanical:
- A
[section]header becomes a JSON object. The bracketed name turns into a key at the top level, and its value is an object that holds everything written under that header. key=valuelines become that object's properties. Each entry inside a section is one property of the section object.key:valueworks the same way, since both separators are common in the wild.- Bare values are strings unless you coerce them. Out of the box,
port=8080gives you the string"8080", not the number8080. INI has no type system, so a faithful reader keeps everything textual until you ask it to do otherwise.
There is one more case worth knowing: keys written before the first [section] header. INI readers treat these as belonging to a default or root scope, so the converter leaves them at the very top level of the JSON object, sitting next to the section objects rather than buried inside one. A stray mode=prod at the top of the file never gets lost.
A worked example
Here is a small but realistic INI file:
# global defaults
mode = prod
[server]
host = localhost
port = 8080 ; listen port
debug = false
[database]
url = postgres://localhost/app
pool = 10
Run it through the INI to JSON converter with type inference turned on and you get:
{
"mode": "prod",
"server": {
"host": "localhost",
"port": 8080,
"debug": false
},
"database": {
"url": "postgres://localhost/app",
"pool": 10
}
}
Notice four things. The mode key stayed at the top level because it appeared before any section. The two [section] headers became nested objects. The # global defaults line and the trailing ; listen port comment both vanished, so you read only the live settings. And because inference was on, 8080 and 10 came through as numbers and false as a real boolean, instead of staying as strings.
Type coercion: the switch that matters most
The single decision that changes your output the most is whether to infer types. With inference off, every value is a string, which is the safe, lossless reading of what the file literally says. With inference on, the converter promotes the obvious candidates: 42 and 10 become numbers, true and false become booleans, and null becomes a JSON null.
This is genuinely useful when you are feeding the result into an app that expects typed config. A Node service that does if (config.server.debug) wants a real boolean, not the truthy string "false" that would quietly enable debug mode forever. Turning inference on saves you a pass of manual cleanup.
But coercion has a sharp edge, and it is worth stating plainly: a value that looks numeric but should stay text gets converted too. A zip code 08055 becomes the number 8055 and loses its leading zero. A version string 1.20 might read as 1.2. The fix is simple once you know to watch for it: either leave inference off for that file, or wrap the specific value in double quotes so it is always read as a string. When I migrate a config I almost always do a first pass with inference off, eyeball anything that looks like an ID, a code, or a version, quote those, and only then flip inference on for the rest.
Going the other way: JSON back to INI
The conversion is reversible, and the reverse direction has its own logic. Every top-level object becomes a [section] block and its inner key=value pairs are printed underneath. Top-level scalars become global keys, printed before any section so they read first. Crucially, a value like "08055" gets wrapped in quotes in the generated INI, so that when something re-parses the file the value stays a string and does not silently turn back into a number.
The one thing INI cannot do is depth. Classic INI is exactly one level of nesting: sections, then keys. So a JSON object whose value is itself an object of objects — three levels down — has nowhere to live, and the converter rejects it rather than inventing a flattened key scheme that nobody would read back correctly. Arrays get the same treatment, because INI has no list syntax. If your JSON has either, you either flatten it to a single section level or keep that part of the config in a JSON-native format. That is not a limitation of the tool; it is the shape of the format.
Migrating a legacy config without retyping
Here is the workflow I actually use when retiring an old config.ini:
- Paste the entire INI in one shot. There is no need to do it section by section — the parser keeps the whole structure.
- Read the output first with inference off, so nothing has changed type yet. This is your ground truth.
- Hunt for values that must stay text: zip codes, account numbers, version strings, anything with a leading zero. Quote them.
- Turn inference on and copy the result straight into
config.json. - If you maintain both formats during a transition, keep JSON as the source of truth and regenerate the INI with the reverse direction whenever it changes.
Because the [section] structure carries over as nested objects, the logical grouping you spent years building survives the move intact. You are not redesigning the config, just changing its serialization.
A side benefit: normalizing two configs to JSON with the same options makes them comparable. Diffing two INI files directly is noisy, since spacing, comment order, and key order all drift. Convert both, paste them into your editor, and a structural diff shows only the real differences in values. Once you have the JSON, the JSON formatter will pretty-print and validate it so the diff is clean and the file is ready to commit.
When this is the wrong tool
One honest caveat. If your config has no [bracketed] sections and instead uses dotted keys like server.port=8080, that is a Java .properties file, not classic INI, and the dots imply nesting that an INI parser will not unpack. Reach for a properties converter in that case. The quick test: brackets mean INI, dots mean properties. Everything in this post assumes the bracketed kind.
For the common case — a real INI file you need to lift into a JSON world — the mapping is small enough to hold in your head: sections become objects, key=value lines become properties, and you decide whether the values stay strings or become typed. Get those three right and the migration is the easy part of the rewrite.
Made by Toolora · Updated 2026-06-13