Skip to content

Commit

Permalink
update readme with some info about the decoder package
Browse files Browse the repository at this point in the history
  • Loading branch information
konnik committed Dec 12, 2023
1 parent 3386a4d commit ca9d986
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 6 deletions.
93 changes: 92 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Kotlin JSON parser (from scratch)

UPDATE: now also includes some nice [decoder combinators](#now-with-decoder-combinators)

[![Java CI with Gradle](https://github.com/konnik/kotlin-json-parser/actions/workflows/gradle.yml/badge.svg)](https://github.com/konnik/kotlin-json-parser/actions/workflows/gradle.yml)

This is a tiny but fully functional JSON parser I hacked together in Kotlin in a
Expand All @@ -20,8 +22,97 @@ and can easily be copied into your own project if you want to use it.

Suggestions are welcome if you think something can be done in a better way.

Please also checkout my other JSON parsers written in [Haskell](https://github.com/konnik/haskell-json-parser/)
Please also checkout my other JSON parsers written in [Haskell](https://github.com/konnik/haskell-json-parser/)
and [Clojure](https://github.com/konnik/clojure-json-parser/).

Happy parsing!

## Now with decoder combinators

After finishing the parser I really wanted to have a go at implementing something
similar to the [Elm JSON Decode package](https://package.elm-lang.org/packages/elm/json/latest/Json.Decode).

I really like the Elm way of decoding JSON by describing the expected data structure using
decoder combinators and mapping the data to the applications domain model.

My implementation have almost all the features of the Elm package and I think it turned out to
be quite nice.

For example consider the following two JSON objects describing two different types of users, guests and registered
users:

Guest:

```json
{
"type": "guest",
"displayName": "Guest123"
}
```

Registered user:

```json
// registeredJson
{
"type": "registered",
"id": 42,
"alias": "mrsmith",
"email": "[email protected]",
"phone": null
}
```

Let's say we want to decode users into the following data structure:

```kotlin
sealed interface User {
data class Guest(val displayName: String) : User
data class Registered(val id: Int, val alias: String, val email: String, val phone: String?) : User
}
```

One way to define a decoder for `User` objects would be like this:

```kotlin
val userDecoder: Decoder<User> =
field("type", str).andThen { type ->
when (type) {
"guest" -> map(
field("displayName") of str,
User::Guest
)

"registered" -> map(
field("id") of int,
field("alias") of str,
field("email") of str,
field("phone") of nullable(str),
User::Registered
)

else -> fail("Invalid type: $type")
}
}
```

This decoder can now be used to parse and decode the users using the `decodeJson` function:

```kotlin
val guest = decodeJson(guestJson, userDecoder)
println(guest)

val registered = decodeJson(registeredJson, userDecoder)
println(registered)
```

This will render the output:

```text
Ok(value=Guest(displayName=Guest123))
Ok(value=Registered(id=42, alias=mrsmith, [email protected], phone=null))
```

Note that I had to implement a simple `Result` type too, of course also heavily inspired
by [the Result type in Elm](https://package.elm-lang.org/packages/elm/core/latest/Result).

12 changes: 7 additions & 5 deletions lib/src/test/kotlin/konnik/json/parser/ParserKtTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,15 @@ class ParserKtTest {
field("type", str).andThen { type ->
when (type) {
"guest" -> map(
field("displayName", str),
field("displayName") of str,
User::Guest
)

"registered" -> map(
field("id", int),
field("alias", str),
field("email", str),
field("phone", nullable(str)),
field("id") of int,
field("alias") of str,
field("email") of str,
field("phone") of nullable(str),
User::Registered
)

Expand All @@ -184,7 +184,9 @@ class ParserKtTest {
}

val guest = decodeJson(guestJson, userDecoder)
println(guest)
val registered = decodeJson(registeredJson, userDecoder)
println(registered)

assertEquals(Ok(User.Guest("Guest123")), guest)
assertEquals(Ok(User.Registered(42, "mrsmith", "[email protected]", null)), registered)
Expand Down

0 comments on commit ca9d986

Please sign in to comment.