JSON CLI Tool
Build a command-line tool that reads a JSON array from stdin and prints a human-readable summary. It’s your “Hello World” for Kotlin — but actually useful, and it exercises a real project from scratch: dependencies, serialization, stdin, data classes, and a few collection idioms.
What you’ll practice
Section titled “What you’ll practice”- Creating a Kotlin Gradle project from scratch
- Adding a dependency (
kotlinx-serialization-json) - Reading stdin, writing stdout
- Core Kotlin:
val/var, string templates, data classes,when/groupBy
Requirements
Section titled “Requirements”- Read a JSON array of objects from stdin.
- Parse the JSON into Kotlin data classes.
- Print a formatted summary to stdout.
Example input
Section titled “Example input”Pipe this to stdin:
[ {"name": "Alice", "language": "Kotlin", "years": 3}, {"name": "Bob", "language": "TypeScript", "years": 5}, {"name": "Charlie", "language": "Go", "years": 2}, {"name": "Diana", "language": "Kotlin", "years": 1}, {"name": "Eve", "language": "TypeScript", "years": 4}]Expected output
Section titled “Expected output”=== Developer Summary ===Total developers: 5
By language: Go: 1 developers (avg 2.0 years) Kotlin: 2 developers (avg 2.0 years) TypeScript: 2 developers (avg 4.5 years)
Most experienced: Bob (TypeScript, 5 years)Least experienced: Diana (Kotlin, 1 years)The worked solution
Section titled “The worked solution”A single-module Gradle project — three files do the whole job:
Directoryjson-cli/
- build.gradle.kts deps + build config
- settings.gradle.kts project name
Directorysrc/main/kotlin/com/example/jsoncli/
- Main.kt the entire program
build.gradle.kts
Section titled “build.gradle.kts”Two plugins matter here: kotlin("jvm") to compile, and
kotlin("plugin.serialization") to turn @Serializable data classes into JSON
parsers at compile time (no reflection). The one runtime dependency is the JSON
library itself.
plugins { kotlin("jvm") version "2.1.0" kotlin("plugin.serialization") version "2.1.0" application}
group = "com.example"version = "1.0-SNAPSHOT"
repositories { mavenCentral()}
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") testImplementation(kotlin("test"))}
tasks.test { useJUnitPlatform()}
application { mainClass.set("com.example.jsoncli.MainKt")}rootProject.name = "json-cli"Main.kt
Section titled “Main.kt”The whole program. A few things to notice if you’re coming from TS/Go:
@Serializableon thedata classis all the JSON wiring you need —Json.decodeFromString<List<Developer>>(...)parses straight into typed objects (likeJSON.parsein TS, but type-checked, orjson.Unmarshalin Go without the struct tags).generateSequence(::readLine)lazily pulls every line from stdin, thenjoinToStringglues them back — that’s the idiomatic “read all of stdin”.groupBy { it.language }returns aMap<String, List<Developer>>; the rest is ordinary collection math.- String templates (
$language,${devs.size}) read like JS template literals.
package com.example.jsoncli
import kotlinx.serialization.Serializableimport kotlinx.serialization.json.Json
@Serializabledata class Developer( val name: String, val language: String, val years: Int)
fun main() { // Read all of stdin into a single string val input = generateSequence(::readLine).joinToString("\n")
if (input.isBlank()) { System.err.println("Error: No input provided. Pipe JSON to stdin.") return }
// Parse JSON into a list of Developer objects val json = Json { ignoreUnknownKeys = true } val developers = try { json.decodeFromString<List<Developer>>(input) } catch (e: Exception) { System.err.println("Error parsing JSON: ${e.message}") return }
if (developers.isEmpty()) { println("No developers found in input.") return }
println("=== Developer Summary ===") println("Total developers: ${developers.size}") println()
// Group by language and print stats println("By language:") developers .groupBy { it.language } .toSortedMap() .forEach { (language, devs) -> val avgYears = devs.map { it.years }.average() println(" $language: ${devs.size} developers (avg ${"%.1f".format(avgYears)} years)") } println()
// Find extremes val mostExperienced = developers.maxByOrNull { it.years }!! val leastExperienced = developers.minByOrNull { it.years }!!
println("Most experienced: ${mostExperienced.name} (${mostExperienced.language}, ${mostExperienced.years} years)") println("Least experienced: ${leastExperienced.name} (${leastExperienced.language}, ${leastExperienced.years} years)")}Run it
Section titled “Run it”-
Build the project:
Terminal window ./gradlew build -
Run with sample data piped to stdin:
Terminal window echo '[{"name":"Alice","language":"Kotlin","years":3},{"name":"Bob","language":"TypeScript","years":5}]' | ./gradlew run --quiet -
Or pipe a file:
Terminal window cat sample.json | ./gradlew run --quiet