Skip to content
This repository has been archived by the owner on Aug 18, 2023. It is now read-only.

kgscialdone/haven-json

Repository files navigation

Build Status Issue Count Contributions Welcome Documentation: Read Now

Haven

Haven is a simple JSON library written in pure Kotlin with no third-party dependencies. It was designed with simplicity of use in mind, with the goal of letting the user manipulate JSON with an almost Javascript-like syntax.

Quick Links:

The Haven Philosophy

  • Simple is better than complex
  • Concise and intuitive is better than verbose and idiomatic
  • Dependencies are a liability
  • Correctness is not the gold standard, it's the baseline

In order to achieve this, Haven:

  • Makes accessing JSON on the fly as simple as possible, providing type safety without forcing the user to think about the static types of their data at every step
  • Provides a DSL syntax as Javascript-like as possible within the restraints of the Kotlin language
  • Uses no third-party dependencies other than the kotlintest testing framework
  • Provides extensive test cases to ensure there are as few bugs and inconsistencies as possible

What makes Haven different?

Haven primarily takes a tree-model approach to dealing with JSON data - parsed JSON data is an in-memory tree of objects that can be deeply accessed on demand. Most JSON libraries available for the JVM take a data-binding approach - you define an object that serves as the structure, which the library then maps the JSON values onto. This difference in approach makes Haven much quicker and simpler to get down and dirty with your data, just like if it was stored in native Map and List objects (because it is!), but it has it's drawbacks as well, namely in type-safety and a hard hit to syntax conciseness. Haven aims to minimize these issues as much as possible, giving you a simple, concise, easy to understand way to interface with your data, while still being type-safe (and without all the type-safety syntax clutter).

Haven also supports a data-binding style of JSON manipulation, similar to most existing JVM libraries; however, this functionality is built on top of the core tree-model system and is really somewhat of a second class citizen. It's recommended to use the tree-model style for simple interactions with your data, and apply the data binding approach primarily when your code needs to interface with other outside code (for example, returning an object instead of raw JSON from an API call convenience method).

Installation

You can install Haven with JitPack (Gradle example shown below), check out the Releases page, or download the source code and build it yourself.

allprojects {
  repositories {
    // ...
    maven { url 'https://jitpack.io' }
  }
}

dependencies {
  implementation 'com.github.swordglowsblue:haven-json:1.1.2'
}

Development To-Do List

  • Basic JSON AST structure
  • DSL for creating JSON objects
  • DSL for accessing JSON objects
  • DSL for modifying JSON objects
  • Methods to convert JSON objects to valid JSON strings
    • Minified
    • Pretty-printed at any indentation size
  • JSON.org spec compliant JSON parser
  • Data-binding style destructuring

Examples

Importing

import com.tripl3dogdare.havenjson.*

Creating JSON via the DSL

val jwick:Json = Json(
  "name" to "John Wick",
  "age" to 37,
  "occupation" to "Hitman",
  "contacts" to listOf(
    mapOf("name" to "Ethan Hunt", "number" to "[REDACTED]"),
    mapOf("name" to "Jason Bourne", "number" to "[REDACTED]"),
    mapOf("name" to "Bender", "number" to "2716057")
  )
)

Accessing JSON via the DSL

// Untyped value access
// Returns null if the value doesn't exist
val jwickName:Any? = jwick["name"].value

// Typed value access
// Returns null if the value doesn't exist or is the wrong type
val jwickName = jwick["name"].value<String>()
val jwickAge= jwick["age"].asInt
val ehuntNumber = jwick["contacts"][0]["number"].asString

Parsing JSON from a string

val string = """
  {
    "name": "John Wick",
    "age": 37,
    "occupation": "Hitman",
    "contacts": [
      { "name": "Ethan Hunt", "number": "[REDACTED]" },
      { "name": "Jason Bourne", "number": "[REDACTED]" },
      { "name": "Bender", "number": "2716057" }
    ]
  }
"""

val jwick:Json = Json.parse(string)

Converting JSON back to a string

val jwickString2 = jwick.mkString())  // Default is 2-space indentation
val jwickString0 = jwick.mkString(0)) // Minified
val jwickString4 = jwick.mkString(4)) // Custom indentation depth in spaces

Deserializing a class instance from JSON

class Field(name:String, value:String) : JsonSchema

// The first parameter can either be a constructor function (::Name) or a class instance (Name::class)
//   The passed function or given class's primary constructor must return a subtype of JsonSchema!
// The second parameter can either be a string (will be parsed as JSON) or an existing Json instance
val field = Json.deserialize(::Field, """{"name":"Test","value":"test"}""")

Deserializing with a name converter function

class Thing(thingName:String) : JsonSchema

// The optional third parameter can be any (String) -> String function
// JsonSchema provides some common defaults like snake case to camel case (shown here)
val field = Json.deserialize(::Thing, """{"thing_name":"Bob"}""", JsonSchema.SNAKE_TO_CAMEL)

Deserializing with overridden property names

class Thing( @JsonProperty("thing_name") thingName:String ) : JsonSchema

// This will do the same as the above; useful for when a particular field doesn't follow conventions
//   or you'd like to explicitly rename something
val field = Json.deserialize(::Thing, """{"thing_name":"Bob"}""")

Custom deserializer functions

class WithDate(date:ZonedDateTime) : JsonSchema

// Register a deserializer function with the default group
// The input and output types of the given function are used to determine what to use
// Only one deserializer function is allowed per output type
Deserializers.add(ZonedDateTime::parse) // (String) -> ZonedDateTime
val withDate = Json.deserialize(::WithDate, """{"date":"2018-07-05T18:13:59+00:00"}""")

// Custom deserializer group
// Useful for having different ways to deserialize a particular output type or not leaking deserializers
//   to other functions
val deser = Deserializers().add(ZonedDateTime::parse)
val withDate = Json.deserialize(::WithDate, """{"date":"2018-07-05T18:13:59+00:00"}""", deserializers = deser)