Generate Kotlin Data Classes From JSON
Kotlin’s data classes are a genuinely nice fit for JSON, less boilerplate than a regular Java class, automatic equals and toString, a handy copy function built in. But there’s a specific way Kotlin’s null safety and JSON parsing libraries can quietly work against each other, and we hit it directly while testing this. Let’s go through what actually works, using a real nested JSON payload compiled and run through the real Kotlin compiler, not just described.
The Sample JSON
Same kind of realistic, slightly tricky payload we’ve used elsewhere, a nested object, an array of primitives, an array of objects, and a nullable field:

{
"id": 1,
"name": "Alex",
"active": true,
"tags": ["admin", "editor"],
"address": { "city": "Lahore", "zip": null },
"orders": [ { "orderId": 101, "total": 49.99 } ]
}
Writing the Data Classes
Same inside-out approach as building any nested type structure, smallest pieces first:
data class Order(
val orderId: Int,
val total: Double
)
data class Address(
val city: String,
val zip: String?
)
data class User(
val id: Int,
val name: String,
val active: Boolean,
val tags: List<String>,
val address: Address,
val orders: List<Order>
)
Notice zip: String?, with the question mark. That’s Kotlin’s null safety syntax, marking this field as allowed to be String or null. Miss that question mark and you’ve told the compiler something that isn’t true about your data, which is exactly the trap covered below.
Parsing Real Data Into It
We used Gson to actually parse the sample JSON into these classes and print the result:
val gson = Gson()
val user = gson.fromJson(json, User::class.java)
println(user)
Real output:
User(id=1, name=Alex, active=true, tags=[admin, editor], address=Address(city=Lahore, zip=null), orders=[Order(orderId=101, total=49.99)])
That readable output didn’t need any code from us. Data classes generate a sensible toString() automatically, which is one of the genuine conveniences over a plain class.
Two Data Class Features Worth Actually Seeing Work
Automatic equals(), not just automatic toString(). We parsed the same JSON twice into two separate objects and compared them:
val user2 = gson.fromJson(json, User::class.java)
println(user == user2)
true
Two completely separate objects, structurally equal, compared correctly with plain ==. A regular Kotlin class without the data keyword would compare by reference instead, and this would print false even though the contents match. Easy to forget how much this actually saves you until you’ve had to manually write an equals() override by hand once.
copy(), for making a modified version without touching the original.
val renamed = user.copy(name = "Sam")
println(renamed.name) // Sam
println(user.name) // Alex, unchanged
Confirmed both lines print exactly what you’d want, a new object with one field changed, the original left completely alone. Useful for working with immutable data without writing a manual copy constructor every time you need a slightly different version of something.
The Gotcha: Null Safety Doesn’t Actually Protect You Here
This is the part worth paying real attention to. Kotlin’s whole pitch is that its type system won’t let a non-nullable value silently become null. We tested what happens when a data class field is typed as non-nullable String, but the actual JSON contains null for that field anyway:
data class AddressBad(
val city: String,
val zip: String // WRONG: not marked nullable, but JSON has null
)
val addr = gson.fromJson(json, AddressBad::class.java)
println(addr.zip) // null
println(addr.zip.length) // crashes here
Here’s what actually happened when we ran it:
zip value: null
Exception in thread "main" java.lang.NullPointerException:
Cannot invoke "String.length()" because the return value of "AddressBad.getZip()" is null
Gson uses reflection to populate these fields directly, which bypasses Kotlin’s compile-time null checks entirely. The compiler thinks zip can never be null, because you told it that with the type. Gson doesn’t care what you told the compiler, it just puts null there anyway, since that’s what the JSON said. The crash doesn’t happen at parse time either, it happens later, the moment something actually tries to use the value as if it were guaranteed non-null. That delay is what makes this genuinely dangerous, the bug is already sitting in your data by the time anything visibly breaks.
The fix is simple once you know to look for it: mark any field nullable if there’s any chance the JSON might actually send null for it, even if you’d prefer it never did. Kotlin’s null safety only protects you as well as your types honestly describe the data.
Frequently Asked Questions
Does this null-safety gap happen with every JSON library, or just Gson?
It’s common to reflection-based libraries generally, since they bypass constructor validation. Libraries built with Kotlin specifically in mind, like kotlinx.serialization, are generally better about respecting nullability correctly, though it’s always worth testing the specific library you’re using rather than assuming.
Why use a data class instead of a regular class for this?
The automatic equals, hashCode, toString, and copy are genuinely useful for objects that represent data rather than behavior, which is exactly what parsed JSON is. A regular class would need all of that written manually to behave the same way.
Should every field be nullable just to be safe?
No, that defeats the purpose of null safety in the first place. Mark a field nullable when the data genuinely can be absent or null, based on what you actually know about the source. Marking everything nullable just to avoid crashes pushes the problem downstream instead of fixing it.
How does this compare to parsing JSON in Java, since Kotlin runs on the same JVM?
The underlying libraries are often the same, Gson works from both. Our comparison of Jackson and Gson in Java covers behavior that applies here too, since both libraries work identically whether called from Java or Kotlin code, the null-safety gap above is specifically a Kotlin-vs-reflection issue, not a Java-vs-Kotlin one.
Summary
Kotlin data classes make working with parsed JSON genuinely pleasant, readable toString, working equals, and copy all come for free. The real trap, confirmed by actually crashing a program on purpose, is that reflection-based parsers like Gson don’t respect Kotlin’s null safety at all. A field typed as non-nullable can still receive null from real JSON, and the compiler has no way to catch it, since the violation happens at runtime, in a library outside Kotlin’s own type checking. Match your nullable types to what the JSON can genuinely send, not what you’d prefer it always sent.
