package jp.juggler.util import java.io.* import java.math.BigDecimal import java.math.BigInteger class JsonException : RuntimeException { constructor(message: String?) : super(message) constructor(message: String?, cause: Throwable?) : super(message, cause) constructor(cause: Throwable) : super(cause.message, cause) } private const val CHAR0 = '\u0000' @Suppress("RegExpUnnecessaryNonCapturingGroup") private val reDecimal = """(?:\A-0\z)|[.eE]""".toRegex() // Tests if the value should be tried as a decimal. // It makes no test if there are actual digits. // return true if the string is "-0" or if it contains '.', 'e', or 'E', false otherwise. fun String.isDecimalNotation(): Boolean = reDecimal.containsMatchIn(this) private fun isNumberStart(initial: Char?) = when (initial) { in '0'..'9' -> true '-' -> true else -> false } private fun String.stringToNumber(): Number { val initial = this.firstOrNull() if (isNumberStart(initial)) { val length = this.length when { isDecimalNotation() -> return if (length > 14) { BigDecimal(this) } else { val d = this.toDouble() if (d.isInfinite() || d.isNaN()) { // if we can't parse it as a double, go up to BigDecimal // this is probably due to underflow like 4.32e-678 // or overflow like 4.65e5324. The size of the string is small // but can't be held in a Double. BigDecimal(this) } else { d } } length <= 9 -> return this.toInt() length <= 18 -> return this.toLong() else -> { // BigInteger version: We use a similar bitLength compare as // BigInteger#intValueExact uses. Increases GC, but objects hold // only what they need. i.e. Less runtime overhead if the value is // long lived. Which is the better tradeoff? This is closer to what's // in stringToValue. val bi = BigInteger(this) return when { bi.bitLength() <= 31 -> bi.toInt() bi.bitLength() <= 63 -> bi.toLong() else -> bi } } } } throw NumberFormatException("val [$this] is not a valid number.") } private fun Any?.asNumber(defaultValue: Number): Number = when (this) { null -> defaultValue is Number -> this else -> try { toString().stringToNumber() } catch (ignored: Throwable) { defaultValue } } class JsonArray : ArrayList { constructor(capacity: Int = 10) : super(capacity) constructor(collection: Collection<*>) : super(collection) constructor(array: Array<*>) : super(array.toList()) fun toString(indentFactor: Int, sort: Boolean = false): String { val sw = StringWriter() synchronized(sw.buffer) { return sw.writeJsonValue(indentFactor, 0, this, sort).toString() } } override fun toString(): String = toString(0) fun objectList() = mapNotNull { it.cast() } fun stringList() = mapNotNull { it?.toString() } fun stringArrayList() = ArrayList(stringList()) fun floatArrayList() = ArrayList(this.size).apply { addAll(this@JsonArray.mapNotNull { this.asNumber(0f).toFloat() }) } fun string(key: Int): String? = this[key]?.toString() fun boolean(key: Int): Boolean? = JsonObject.castBoolean(this[key]) fun int(key: Int): Int? = JsonObject.castInt(this[key]) fun long(key: Int): Long? = JsonObject.castLong(this[key]) fun float(key: Int): Float? = JsonObject.castFloat(this[key]) @Suppress("MemberVisibilityCanBePrivate") fun double(key: Int): Double? = JsonObject.castDouble(this[key]) fun jsonObject(key: Int) = this[key].cast() @Suppress("MemberNameEqualsClassName") fun jsonArray(key: Int) = this[key].cast() fun optString(key: Int, defVal: String = "") = string(key) ?: defVal fun optBoolean(key: Int, defVal: Boolean = false) = boolean(key) ?: defVal @Suppress("unused") fun optInt(key: Int, defVal: Int = 0) = int(key) ?: defVal @Suppress("unused") fun optLong(key: Int, defVal: Long = 0L) = long(key) ?: defVal @Suppress("unused") fun optFloat(key: Int, defVal: Float = 0f) = float(key) ?: defVal @Suppress("unused") fun optDouble(key: Int, defVal: Double = 0.0) = double(key) ?: defVal @Suppress("unused") fun notEmptyOrThrow(key: Int) = notEmptyOrThrow(key.toString(), string(key)) @Suppress("unused") fun isNull(key: Int) = this[key] == null } // https://stackoverflow.com/questions/5525795/does-javascript-guarantee-object-property-order/38218582#38218582 // ブラウザはES2015によりオブジェクト列挙順序に挿入順序が影響する // JSONにそんな規定はないが、MisskeyのAPIはこれに依存した挙動をする // https://github.com/syuilo/misskey/issues/5684 class JsonObject : LinkedHashMap() { companion object { fun castBoolean(o: Any?): Boolean? = when (o) { null -> null is Boolean -> o is Int -> o != 0 is Long -> o != 0L is Float -> !(o.isFinite() && o == 0f) is Double -> !(o.isFinite() && o == 0.0) is String -> when (o) { "", "0", "false", "False" -> false else -> true } is JsonArray -> o.isNotEmpty() is JsonObject -> o.isNotEmpty() else -> true } fun castLong(o: Any?): Long? = when (o) { is Long -> o is Number -> o.toLong() is String -> try { o.stringToNumber().toLong() } catch (_: NumberFormatException) { null } else -> null // may null or JsonObject.NULL or object,array,boolean } fun castInt(o: Any?): Int? = when (o) { is Int -> o is Number -> try { o.toInt() } catch (_: NumberFormatException) { null } is String -> try { o.stringToNumber().toInt() } catch (_: NumberFormatException) { null } else -> null // may null or JsonObject.NULL or object,array,boolean } fun castDouble(o: Any?): Double? = when (o) { is Double -> o is Number -> try { o.toDouble() } catch (_: NumberFormatException) { null } is String -> try { o.stringToNumber().toDouble() } catch (_: NumberFormatException) { null } else -> null // may null or JsonObject.NULL or object,array,boolean } fun castFloat(o: Any?): Float? = when (o) { is Float -> o is Number -> try { o.toFloat() } catch (_: NumberFormatException) { null } is String -> try { o.stringToNumber().toFloat() } catch (_: NumberFormatException) { null } else -> null // may null or JsonObject.NULL or object,array,boolean } } fun toString(indentFactor: Int, sort: Boolean = false): String { val sw = StringWriter() synchronized(sw.buffer) { return sw.writeJsonValue(indentFactor, 0, this, sort = sort).toString() } } override fun toString(): String = toString(0) fun string(key: String): String? = this[key]?.toString() fun boolean(key: String): Boolean? = castBoolean(this[key]) fun int(key: String): Int? = castInt(this[key]) fun long(key: String): Long? = castLong(this[key]) fun float(key: String): Float? = castFloat(this[key]) fun double(key: String): Double? = castDouble(this[key]) @Suppress("MemberNameEqualsClassName") fun jsonObject(name: String) = this[name].cast() fun jsonArray(name: String) = this[name].cast() fun stringArrayList(name: String): ArrayList? = jsonArray(name)?.stringArrayList()?.notEmpty() fun floatArrayList(name: String): ArrayList? = jsonArray(name)?.floatArrayList()?.notEmpty() fun optString(name: String, defVal: String = "") = string(name) ?: defVal fun optBoolean(name: String, defVal: Boolean = false) = boolean(name) ?: defVal fun optInt(name: String, defVal: Int = 0) = int(name) ?: defVal fun optLong(name: String, defVal: Long = 0L) = long(name) ?: defVal fun optFloat(name: String, defVal: Float = 0f) = float(name) ?: defVal @Suppress("unused") fun optDouble(name: String, defVal: Double = 0.0) = double(name) ?: defVal fun stringOrThrow(name: String) = notEmptyOrThrow(name, string(name)) // fun isNull(name : String) = this[name] == null fun putNotNull(name: String, value: Any?) { if (value != null) put(name, value) } fun putIfNotDefault(key: String, value: Boolean, defVal: Boolean?) { if (value != defVal) put(key, value) } fun putIfTrue(key: String, value: Boolean) { if (value) put(key, value) } } class JsonTokenizer(reader: Reader) { companion object { private fun String.toStringOrNumber(): Any { /* * If it might be a number, try converting it. If a number cannot be * produced, then the value will just be a string. */ val initial = this.firstOrNull() if (initial != null && (initial in '0'..'9' || initial == '-')) { try { // if we want full Big Number support the contents of this // `try` block can be replaced with: // return stringToNumber(string); if (isDecimalNotation()) { val d = toDouble() if (!d.isInfinite() && !d.isNaN()) { return d } } else { val longValue = toLong() if (longValue.toString() == this) { try { val intValue = longValue.toInt() if (intValue.toLong() == longValue) return intValue } catch (_: Throwable) { // ignored } return longValue } } } catch (ignore: Exception) { } } return this } } // constructor(inputStream : InputStream) : this(InputStreamReader(inputStream)) constructor(s: String) : this(StringReader(s)) /** current read character position on the current line. */ private var character = 1L /** flag to indicate if the end of the input has been found. */ private var eof = false /** current read index of the input. */ private var index = 0L /** current line of the input. */ private var line = 1L /** previous character read from the input. */ private var previous = CHAR0 /** Reader for the input. */ private val reader = if (reader.markSupported()) reader else BufferedReader(reader) /** flag to indicate that a previous character was requested. */ private var usePrevious = false /** the number of characters read in the previous line. */ private var characterPreviousLine = 0L /** * Back up one character. This provides a sort of lookahead capability, * so that you can test for a digit or letter before attempting to parse * the next number or identifier. * @throws JsonException Thrown if trying to step back more than 1 step * or if already at the start of the string */ private fun back() { if (usePrevious || index <= 0) { throw JsonException("Stepping back two steps is not supported") } decrementIndexes() usePrevious = true eof = false } /** * Decrements the indexes for the [.back] method based on the previous character read. */ private fun decrementIndexes() { index-- if (previous == '\r' || previous == '\n') { line-- character = characterPreviousLine } else if (character > 0) { character-- } } /** * Checks if the end of the input has been reached. * * @return true if at the end of the file and we didn't step back */ private fun end(): Boolean { return eof && !usePrevious } // /** // * Determine if the source string still contains characters that next() // * can consume. // * @return true if not yet at the end of the source. // * @throws JsonException thrown if there is an error stepping forward // * or backward while checking for more data. // */ // private fun more() : Boolean { // if(usePrevious) { // return true // } // try { // reader.mark(1) // } catch(e : IOException) { // throw JsonException("Unable to preserve stream position", e) // } // try { // -1 is EOF, but next() can not consume the null character '\0' // if(reader.read() <= 0) { // eof = true // return false // } // reader.reset() // } catch(e : IOException) { // throw JsonException("Unable to read the next character from the stream", e) // } // return true // } /** * Get the next character in the source string. * * @return The next character, or 0 if past the end of the source string. * @throws JsonException Thrown if there is an error reading the source string. */ private operator fun next(): Char { val c: Char if (usePrevious) { usePrevious = false c = previous } else { val i = try { reader.read() } catch (exception: IOException) { throw JsonException(exception) } if (i <= 0) { // End of stream eof = true return CHAR0 } c = i.toChar() } incrementIndexes(c) previous = c return c } /** * Increments the internal indexes according to the previous character * read and the character passed as the current character. * @param c the current character read. */ private fun incrementIndexes(c: Char) { if (c == CHAR0) return index++ when (c) { '\r' -> { line++ characterPreviousLine = character character = 0 } '\n' -> { if (previous != '\r') { line++ characterPreviousLine = character } character = 0 } else -> { character++ } } } // /** // * Consume the next character, and check that it matches a specified // * character. // * @param c The character to match. // * @return The character. // * @throws JsonException if the character does not match. // */ // private fun next(c : Char) : Char { // val n = this.next() // if(n != c) { // if(n.toInt() > 0) { // throw this.syntaxError( // "Expected '" + c + "' and instead saw '" + // n + "'" // ) // } // throw this.syntaxError("Expected '$c' and instead saw ''") // } // return n // } /** * Get the next n characters. * * @param n The number of characters to take. * @return A string of n characters. * @throws JsonException * Substring bounds error if there are not * n characters remaining in the source string. */ private fun next(@Suppress("SameParameterValue") n: Int): String { if (n == 0) { return "" } val chars = CharArray(n) var pos = 0 while (pos < n) { chars[pos] = this.next() if (end()) { throw this.syntaxError("Substring bounds error") } pos += 1 } return String(chars) } /** * Get the next char in the string, skipping whitespace. * @throws JsonException Thrown if there is an error reading the source string. * @return A character, or 0 if there are no more characters. */ private fun nextClean(): Char { while (true) { val c = this.next() if (c == CHAR0 || c > ' ') { return c } } } /** * Return the characters up to the next close quote character. * Backslash processing is done. The formal JSON format does not * allow strings in single quotes, but an implementation is allowed to * accept them. * @param quote The quoting character, either * `"` (double quote) or * `'` (single quote). * @return A String. * @throws JsonException Unterminated string. */ private fun nextString(quote: Char): String { val sb = StringBuilder() while (true) { var c: Char = this.next() when (c) { CHAR0, '\n', '\r' -> throw this.syntaxError("Unterminated string") quote -> return sb.toString() '\\' -> { c = this.next() when (c) { 'b' -> sb.append('\b') 't' -> sb.append('\t') 'n' -> sb.append('\n') 'f' -> sb.append('\u000c') 'r' -> sb.append('\r') 'u' -> try { sb.append(this.next(4).toInt(16).toChar()) } catch (e: NumberFormatException) { throw syntaxError("Illegal escape.", e) } '"', '\'', '\\', '/' -> sb.append(c) else -> throw syntaxError("Illegal escape.") } } else -> sb.append(c) } } } // /** // * Get the text up but not including the specified character or the // * end of line, whichever comes first. // * @param delimiter A delimiter character. // * @return A string. // * @throws JsonException Thrown if there is an error while searching // * for the delimiter // */ // fun nextTo(delimiter : Char) : String { // val sb = StringBuilder() // while(true) { // val c = this.next() // if(c == delimiter || c == char0 || c == '\n' || c == '\r') { // if(c != char0) { // back() // } // return sb.toString().trim() // } // sb.append(c) // } // } // /** // * Get the text up but not including one of the specified delimiter // * characters or the end of line, whichever comes first. // * @param delimiters A set of delimiter characters. // * @return A string, trimmed. // * @throws JsonException Thrown if there is an error while searching // * for the delimiter // */ // fun nextTo(delimiters : String) : String { // val sb = StringBuilder() // while(true) { // val c = this.next() // if(delimiters.indexOf(c) >= 0 || c == char0 || c == '\n' || c == '\r') { // if(c != char0) { // back() // } // return sb.toString().trim { it <= ' ' } // } // sb.append(c) // } // } /** * Get the next value. The value can be a Boolean, Double, Integer, * JsonArray, JsonObject, Long, or String, or the JsonObject.NULL object. * @throws JsonException If syntax error. * * @return An object. */ fun nextValue(): Any? { var c = nextClean() val string: String when (c) { '"', '\'' -> return nextString(c) '{' -> { back() return parseInto(JsonObject()) } '[' -> { back() return parseInto(JsonArray()) } } /* * Handle unquoted text. This could be the values true, false, or * null, or it can be a number. An implementation (such as this one) * is allowed to also accept non-standard forms. * * Accumulate characters until we reach the end of the text or a * formatting character. */ val sb = StringBuilder() while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { sb.append(c) c = this.next() } if (!eof) { back() } string = sb.toString().trim { it <= ' ' } if ("" == string) { throw syntaxError("Missing value") } return with(string) { when { isEmpty() -> "" equals("true", ignoreCase = true) -> true equals("false", ignoreCase = true) -> false equals("null", ignoreCase = true) -> null else -> toStringOrNumber() } } } // /** // * Skip characters until the next character is the requested character. // * If the requested character is not found, no characters are skipped. // * @param to A character to skip to. // * @return The requested character, or zero if the requested character // * is not found. // * @throws JsonException Thrown if there is an error while searching // * for the to character // */ // @Throws(JsonException::class) // fun skipTo(to : Char) : Char { // var c : Char // try { // val startIndex = index // val startCharacter = character // val startLine = line // reader.mark(1000000) // do { // c = this.next() // if(c.toInt() == 0) { // in some readers, reset() may throw an exception if // // the remaining portion of the input is greater than // // the mark size (1,000,000 above). // reader.reset() // index = startIndex // character = startCharacter // line = startLine // return char0 // } // } while(c != to) // reader.mark(1) // } catch(exception : IOException) { // throw JsonException(exception) // } // back() // return c // } /** * Make a JsonException to signal a syntax error. * * @param message The error message. * @return A JsonException object, suitable for throwing */ private fun syntaxError(message: String): JsonException { return JsonException(message + this.toString()) } /** * Make a JsonException to signal a syntax error. * * @param message The error message. * @param causedBy The throwable that caused the error. * @return A JsonException object, suitable for throwing */ private fun syntaxError( @Suppress("SameParameterValue") message: String, causedBy: Throwable?, ) = JsonException(message + toString(), causedBy) /** * Make a printable string of this JSONTokener. * * @return " at {index} [character {character} line {line}]" */ override fun toString(): String = " at $index [character $character line $line]" private fun parseInto(dst: JsonObject): JsonObject { if (nextClean() != '{') throw syntaxError("A JsonObject text must begin with '{'") while (true) { var c: Char = nextClean() val key: String = when (c) { CHAR0 -> throw syntaxError("A JsonObject text must end with '}'") '}' -> return dst else -> { back() nextValue().toString() } } // The key is followed by ':'. c = nextClean() if (c != ':') throw syntaxError("Expected a ':' after a key") // Use syntaxError(..) to include error location // Check if key exists // key already exists if (dst.contains(key)) throw syntaxError("Duplicate key \"$key\"") // Only add value if non-null dst[key] = nextValue() when (nextClean()) { ';', ',' -> { if (nextClean() == '}') return dst back() } '}' -> return dst else -> throw syntaxError("Expected a ',' or '}'") } } } private fun parseInto(dst: JsonArray): JsonArray { if (nextClean() != '[') throw syntaxError("A JsonArray text must start with '['") when (nextClean()) { // array is unclosed. No ']' found, instead EOF CHAR0 -> throw syntaxError("Expected a ',' or ']'") // empty array ']' -> return dst else -> { back() while (true) { if (nextClean() == ',') { back() dst.add(null) } else { back() dst.add(nextValue()) } when (nextClean()) { CHAR0 -> throw syntaxError("Expected a ',' or ']'") ']' -> return dst ',' -> when (nextClean()) { // array is unclosed. No ']' found, instead EOF CHAR0 -> throw syntaxError("Expected a ',' or ']'") ']' -> return dst else -> back() } else -> throw syntaxError("Expected a ',' or ']'") } } } } } } private val reNumber = """-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?""".asciiPattern() private fun Writer.writeQuote(string: String): Writer { if (string.isEmpty()) { write("\"\"") } else { append('"') var previousChar: Char = CHAR0 for (c in string) { when (c) { '\\', '"' -> { append('\\') append(c) } '/' -> { if (previousChar == '<') append('\\') append(c) } '\b' -> append("\\b") '\t' -> append("\\t") '\n' -> append("\\n") '\u000c' -> append("\\f") '\r' -> append("\\r") in CHAR0 until ' ', in '\u0080' until '\u00a0', in '\u2000' until '\u2100', -> { write("\\u") val hexCode: String = Integer.toHexString(c.code) write("0000", 0, 4 - hexCode.length) write(hexCode) } else -> append(c) } previousChar = c } append('"') } return this } private fun Number.toJsonString(): String { when (this) { is Double -> if (isInfinite() || isNaN()) throw JsonException("JSON does not allow non-finite numbers.") is Float -> if (isInfinite() || isNaN()) throw JsonException("JSON does not allow non-finite numbers.") } // Shave off trailing zeros and decimal point, if possible. var string = toString() if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && string.indexOf('E') < 0 ) { while (string.endsWith("0")) { string = string.substring(0, string.length - 1) } if (string.endsWith(".")) { string = string.substring(0, string.length - 1) } } return string } private fun Writer.indent(indentFactor: Int, indent: Int): Writer { if (indentFactor > 0) { append('\n') for (i in 0 until indent) append(' ') } return this } private fun Writer.writeCollection( indentFactor: Int, indent: Int, src: Collection<*>, sort: Boolean ): Writer = try { append('[') when (src.size) { 0 -> { } 1 -> try { writeJsonValue(indentFactor, indent, src.iterator().next(), sort) } catch (e: Exception) { throw JsonException("Unable to write JsonArray value at index: 0", e) } else -> { val newIndent = indent + indentFactor for ((index, value) in src.withIndex()) { if (index > 0) append(',') indent(indentFactor, newIndent) try { writeJsonValue(indentFactor, newIndent, value, sort) } catch (ex: Exception) { throw JsonException("Unable to write JsonArray value at index: $index", ex) } } indent(indentFactor, indent) } } append(']') this } catch (e: IOException) { throw JsonException(e) } private fun Writer.writeArray(indentFactor: Int, indent: Int, src: Any, sort: Boolean): Writer = try { append('[') when (val size = java.lang.reflect.Array.getLength(src)) { 0 -> { } 1 -> try { val value = java.lang.reflect.Array.get(src, 0) writeJsonValue(indentFactor, indent, value, sort) } catch (e: Exception) { throw JsonException("Unable to write JsonArray value at index: 0", e) } else -> { val newIndent = indent + indentFactor for (index in 0 until size) { if (index > 0) append(',') indent(indentFactor, newIndent) try { val value = java.lang.reflect.Array.get(src, index) writeJsonValue(indentFactor, newIndent, value, sort) } catch (ex: Exception) { throw JsonException("Unable to write JsonArray value at index: $index", ex) } } indent(indentFactor, indent) } } append(']') this } catch (e: IOException) { throw JsonException(e) } private fun Writer.writeMap(indentFactor: Int, indent: Int, src: Map<*, *>, sort: Boolean): Writer = try { append('{') when (src.size) { 0 -> { } 1 -> { val entry = src.entries.first() writeJsonValue(indentFactor, indent, entry.key, sort) append(':') if (indentFactor > 0) append(' ') try { writeJsonValue(indentFactor, indent, entry.value, sort) } catch (ex: Throwable) { throw JsonException( "Unable to write JsonObject value for key: ${entry.key}", ex ) } } else -> { val newIndent = indent + indentFactor var needsComma = false val entries = if (sort) { src.entries.sortedBy { it.key.toString() } } else { src.entries } for (entry in entries) { if (needsComma) append(',') indent(indentFactor, newIndent) writeJsonValue(indentFactor, newIndent, entry.key, sort) append(':') if (indentFactor > 0) append(' ') try { writeJsonValue(indentFactor, newIndent, entry.value, sort) } catch (ex: Exception) { throw JsonException( "Unable to write JsonObject value for key: ${entry.key}", ex ) } needsComma = true } indent(indentFactor, indent) } } append('}') this } catch (e: IOException) { throw JsonException(e) } fun Writer.writeJsonValue( indentFactor: Int, indent: Int, value: Any?, sort: Boolean, ): Writer { when { value == null -> write("null") value is Boolean -> write(value.toString()) value is Number -> { val sv = value.toJsonString() if (reNumber.matcher(sv).matches()) { write(sv) } else { // not all Numbers may match actual JSON Numbers. i.e. fractions or Imaginary // The Number value is not a valid JSON number. // Instead we will quote it as a string writeQuote(sv) } } value is Char -> writeJsonValue(indentFactor, indent, value.code, sort = sort) value is String -> writeQuote(value) value is Enum<*> -> writeQuote(value.name) value is JsonObject -> writeMap(indentFactor, indent, value, sort = sort) value is Map<*, *> -> writeMap(indentFactor, indent, value, sort = sort) value is Collection<*> -> writeCollection(indentFactor, indent, value, sort = sort) value.javaClass.isArray -> writeArray(indentFactor, indent, value, sort = sort) else -> writeQuote(value.toString()) } return this } ///////////////////////////////////////////////////////////////////////////// fun notEmptyOrThrow(name: String, value: String?) = if (value?.isNotEmpty() == true) value else error("$name is empty") private val log = LogCategory("Json") // return null if the json value is "null" fun String.decodeJsonValue() = try { JsonTokenizer(this).nextValue() } catch (ex: Throwable) { log.e(ex, "decodeJsonValue failed. $this") throw ex } //fun String.parseJsonOrNull() =try { // decodeJsonValue() //}catch(ex:Throwable){ // log.e(ex,"decodeJsonValue() failed.") // null //} fun String.decodeJsonObject() = decodeJsonValue()!!.castNotNull() fun String.decodeJsonArray() = decodeJsonValue()!!.castNotNull() fun Array<*>.toJsonArray(): JsonArray = JsonArray(this) fun List<*>.toJsonArray() = JsonArray(this) inline fun jsonObject(initializer: JsonObject.() -> Unit) = JsonObject().apply { initializer() } inline fun jsonArray(initializer: JsonArray.() -> Unit) = JsonArray().apply { initializer() } fun jsonArrayOf(vararg args: Any) = JsonArray(args) fun jsonObjectOf(vararg args: Pair) = JsonObject().apply { for (pair in args) { put(pair.first, pair.second) } }