Converting a Java .properties File to JSON Without Losing a Key
A practical guide to turning Java and Spring .properties files into JSON: how dotted keys nest, how escaping works, and how to migrate config safely.
Converting a Java .properties File to JSON Without Losing a Key
I have spent more hours than I would like staring at a .properties file, trying to convince myself that the JSON I hand-typed from it actually matches. The format looks trivial: a key, a separator, a value, one per line. But once you start migrating real Spring config, the small rules pile up. A colon is also a separator. A trailing backslash eats the next line. And port=8080 is a string, not the number you assumed. This guide walks through what actually happens when you convert a .properties file to JSON, and where the silent mistakes hide.
If you just want to do it right now, paste your file into the Properties to JSON Converter and read the JSON on the right. The rest of this post explains why it does what it does.
How key=value lines become JSON
A .properties file is a flat list of assignments. Each non-empty, non-comment line has a key, a separator, and a value:
app.name=Checkout Service
server.port=8080
The separator can be =, :, or even whitespace. So server.port=8080, server.port:8080, and server.port 8080 all parse to the same key/value pair. Lines that start with # or ! are comments and get dropped, because JSON has no comment syntax. Blank lines are ignored.
The mechanical conversion is direct: every key becomes a JSON property, every value becomes a JSON string. That last word matters. There are no types in .properties. The reader stores text. So a flat conversion of the two lines above gives you:
{
"app.name": "Checkout Service",
"server.port": "8080"
}
Note "8080" in quotes. That is correct, and I will come back to why guessing would be worse.
Dotted keys and nested objects
Spring developers do not really think of server.port as a flat key named "server.port". They think of server as an object with a port field, because that is how relaxed binding reads config into @ConfigurationProperties beans. The converter can expand dotted keys the same way.
With dotted-key expansion on (the default), server.port=8080 builds a tree:
{
"server": {
"port": "8080"
}
}
Each segment between dots becomes one level of nesting. So a.b.c=1 produces {"a":{"b":{"c":"1"}}}. This is what you want when the JSON is going to feed a system that expects the same nested shape Spring binds to.
Turn the toggle off and the dot stays literal: server.port remains a single flat string key "server.port". That is the right choice when a downstream consumer wants literal property names rather than a tree, for example a tool that reads a flat map and does its own prefix matching. The point is that nesting is a presentation decision, not a parsing one, so you get to pick.
Escaping, separators, and line continuations
This is the section that bites people. The .properties format inherits a set of escape rules from java.util.Properties, and the converter honors them so the JSON matches what the JVM would have loaded.
\n,\t,\uXXXXare real escape sequences.msg=line1\nline2carries an actual newline into the value.\=and\:let you put a literal equals or colon inside a key. Without the backslash, the first unescaped=or:is treated as the separator. Sotimeout:30is the keytimeoutwith value30, not a key literally namedtimeout:30.- A trailing backslash continues the line. A line ending in a single
\joins with the next line, and leading whitespace on the continued line is stripped.
That last rule is the one that causes head-scratching. Consider:
fruits=apple,\
banana,cherry
The \ at the end of the first line tells the reader to keep going, so the value is the single string apple,banana,cherry. This is great for wrapping a long classpath or comma list across readable lines. It is also a trap: an accidental backslash at the end of a value silently swallows the next line into that value. If you want a literal backslash at the end, write \\.
A worked example
Here is a small but realistic chunk of a Spring Boot application.properties:
# datasource
spring.datasource.url=jdbc:postgresql://db:5432/shop
spring.datasource.username=app
server.port=8080
logging.level.root=INFO
feature.flags=beta,\
metrics
Convert that with dotted-key expansion on, and you get:
{
"spring": {
"datasource": {
"url": "jdbc:postgresql://db:5432/shop",
"username": "app"
}
},
"server": {
"port": "8080"
},
"logging": {
"level": {
"root": "INFO"
}
},
"feature": {
"flags": "beta,metrics"
}
}
A few things to notice. The comment line vanished. The colons inside the JDBC URL stayed put, because the first = already claimed the separator role, so everything after it is the value. server.port and logging.level.root folded into their proper trees. And the continued feature.flags line collapsed into one value, beta,metrics, with the indentation stripped.
Why values stay strings
The single most common surprise is that port=8080 comes back as "8080" rather than 8080. This is deliberate, and it is the safe default. The .properties format has no way to declare that a value is a number, a boolean, or anything other than text. If the converter guessed types, it would eventually guess wrong: a version string like 1.10 would silently become the number 1.1, and a zip code 01234 would lose its leading zero and become 1234. Preserving everything as a string keeps the round trip faithful. You cast to the real type in your application code, where you actually know what 8080 is supposed to mean.
The conversion runs the other way too. Paste JSON, switch direction, and nested objects flatten back to dotted keys with equals signs and unicode escaped, producing a file java.util.Properties accepts without edits. Because nesting expands one way and flattens back the other, a config can survive the round trip unchanged.
Migrating Spring config in practice
When you are moving a service from application.properties to a JSON config blob, the workflow is short: paste the whole file, keep dotted-key expansion on, and copy the nested JSON into your values file. The structure maps onto the same beans, so server.port and spring.datasource.url land where the binder expects them. One caveat worth remembering: placeholders like ${OTHER_HOST} are left as literal text, because resolving them needs the full runtime environment that a static file does not have. Profile files such as application-prod.properties convert the same way, one at a time.
If you want to sanity-check the output or diff two environments, paste the JSON into the JSON Formatter and the structural comparison shows exactly which database URL or feature flag changed, free of the line-ordering noise that makes raw .properties diffs so painful. That two-step move, convert then format, has saved me from shipping a config that dropped a key more than once.
Made by Toolora · Updated 2026-06-13