1+ package io.github.json5.kotlin
2+
3+ import kotlinx.serialization.*
4+ import kotlinx.serialization.descriptors.*
5+ import kotlinx.serialization.encoding.*
6+ import kotlinx.serialization.json.*
7+ import kotlinx.serialization.modules.*
8+
9+ /* *
10+ * JSON5 serialization format for kotlinx.serialization.
11+ *
12+ * This format allows encoding and decoding of @Serializable classes to/from JSON5 format.
13+ * It builds on top of the existing JSON5Parser and JSON5Serializer implementations.
14+ */
15+ @OptIn(ExperimentalSerializationApi ::class )
16+ class JSON5Format (
17+ private val configuration : JSON5Configuration = JSON5Configuration .Default
18+ ) : StringFormat {
19+
20+ override val serializersModule: SerializersModule = EmptySerializersModule ()
21+
22+ /* *
23+ * Encodes the given [value] to JSON5 string using the serializer retrieved from reified type parameter.
24+ */
25+ override fun <T > encodeToString (serializer : SerializationStrategy <T >, value : T ): String {
26+ val jsonElement = JSON5Encoder (configuration).encodeToJsonElement(serializer, value)
27+ return jsonElementToJson5String(jsonElement)
28+ }
29+
30+ /* *
31+ * Decodes the given JSON5 [string] to a value of type [T] using the given [deserializer].
32+ */
33+ override fun <T > decodeFromString (deserializer : DeserializationStrategy <T >, string : String ): T {
34+ val jsonElement = json5StringToJsonElement(string)
35+ return JSON5Decoder (configuration, jsonElement).decodeSerializableValue(deserializer)
36+ }
37+
38+ private fun jsonElementToJson5String (element : JsonElement ): String {
39+ // Convert JsonElement to a regular Kotlin object that JSON5Serializer can handle
40+ val kotlinObject = jsonElementToKotlinObject(element)
41+ return JSON5Serializer .stringify(kotlinObject, configuration.prettyPrint)
42+ }
43+
44+ private fun json5StringToJsonElement (string : String ): JsonElement {
45+ // Parse JSON5 string to Kotlin object, then convert to JsonElement
46+ val kotlinObject = JSON5Parser .parse(string)
47+ return kotlinObjectToJsonElement(kotlinObject)
48+ }
49+
50+ private fun jsonElementToKotlinObject (element : JsonElement ): Any? {
51+ return when (element) {
52+ is JsonNull -> null
53+ is JsonPrimitive -> when {
54+ element.isString -> element.content
55+ element.content == " true" -> true
56+ element.content == " false" -> false
57+ else -> {
58+ // Try to preserve the original numeric type
59+ val content = element.content
60+ // Handle scientific notation that should be parsed as Long
61+ if (content.contains(' E' ) || content.contains(' e' )) {
62+ val doubleValue = content.toDoubleOrNull()
63+ if (doubleValue != null && doubleValue.isFinite() && doubleValue % 1.0 == 0.0 ) {
64+ when {
65+ doubleValue >= Int .MIN_VALUE && doubleValue <= Int .MAX_VALUE -> doubleValue.toInt()
66+ doubleValue >= Long .MIN_VALUE && doubleValue <= Long .MAX_VALUE -> doubleValue.toLong()
67+ else -> doubleValue
68+ }
69+ } else {
70+ doubleValue ? : content
71+ }
72+ } else {
73+ content.toIntOrNull()
74+ ? : content.toLongOrNull()
75+ ? : content.toDoubleOrNull()
76+ ? : content
77+ }
78+ }
79+ }
80+ is JsonObject -> element.mapValues { jsonElementToKotlinObject(it.value) }
81+ is JsonArray -> element.map { jsonElementToKotlinObject(it) }
82+ }
83+ }
84+
85+ private fun kotlinObjectToJsonElement (obj : Any? ): JsonElement {
86+ return when (obj) {
87+ null -> JsonNull
88+ is Boolean -> JsonPrimitive (obj)
89+ is Int -> JsonPrimitive (obj)
90+ is Long -> JsonPrimitive (obj)
91+ is Double -> {
92+ // If the double is actually a whole number, try to represent it as int or long if it fits
93+ if (obj.isFinite() && obj % 1.0 == 0.0 ) {
94+ when {
95+ obj >= Int .MIN_VALUE && obj <= Int .MAX_VALUE -> JsonPrimitive (obj.toInt())
96+ obj >= Long .MIN_VALUE && obj <= Long .MAX_VALUE -> JsonPrimitive (obj.toLong())
97+ else -> JsonPrimitive (obj)
98+ }
99+ } else {
100+ JsonPrimitive (obj)
101+ }
102+ }
103+ is Float -> {
104+ // Similar handling for float
105+ if (obj.isFinite() && obj % 1.0f == 0.0f ) {
106+ when {
107+ obj >= Int .MIN_VALUE && obj <= Int .MAX_VALUE -> JsonPrimitive (obj.toInt())
108+ obj >= Long .MIN_VALUE && obj <= Long .MAX_VALUE -> JsonPrimitive (obj.toLong())
109+ else -> JsonPrimitive (obj)
110+ }
111+ } else {
112+ JsonPrimitive (obj)
113+ }
114+ }
115+ is Number -> JsonPrimitive (obj)
116+ is String -> JsonPrimitive (obj)
117+ is Map <* , * > -> {
118+ val jsonObject = mutableMapOf<String , JsonElement >()
119+ @Suppress(" UNCHECKED_CAST" )
120+ val map = obj as Map <String , Any ?>
121+ for ((key, value) in map) {
122+ jsonObject[key] = kotlinObjectToJsonElement(value)
123+ }
124+ JsonObject (jsonObject)
125+ }
126+ is List <* > -> {
127+ JsonArray (obj.map { kotlinObjectToJsonElement(it) })
128+ }
129+ else -> JsonPrimitive (obj.toString())
130+ }
131+ }
132+ }
133+
134+ /* *
135+ * Configuration for JSON5 serialization.
136+ */
137+ data class JSON5Configuration (
138+ val prettyPrint : Boolean = false ,
139+ val prettyPrintIndent : String = " "
140+ ) {
141+ companion object {
142+ val Default = JSON5Configuration ()
143+ }
144+ }
145+
146+ /* *
147+ * Encoder implementation that uses kotlinx.serialization's JSON encoder as a bridge.
148+ */
149+ private class JSON5Encoder (private val configuration : JSON5Configuration ) {
150+ private val json = Json {
151+ encodeDefaults = true
152+ isLenient = true
153+ allowSpecialFloatingPointValues = true
154+ }
155+
156+ fun <T > encodeToJsonElement (serializer : SerializationStrategy <T >, value : T ): JsonElement {
157+ return json.encodeToJsonElement(serializer, value)
158+ }
159+ }
160+
161+ /* *
162+ * Decoder implementation that uses kotlinx.serialization's JSON decoder as a bridge.
163+ */
164+ private class JSON5Decoder (
165+ private val configuration : JSON5Configuration ,
166+ private val element : JsonElement
167+ ) {
168+ private val json = Json {
169+ ignoreUnknownKeys = true
170+ isLenient = true
171+ allowSpecialFloatingPointValues = true
172+ }
173+
174+ fun <T > decodeSerializableValue (deserializer : DeserializationStrategy <T >): T {
175+ return json.decodeFromJsonElement(deserializer, element)
176+ }
177+ }
178+
179+ /* *
180+ * Default JSON5 format instance.
181+ */
182+ val DefaultJSON5Format = JSON5Format ()
0 commit comments