Skip to content

Commit 19facf3

Browse files
authored
Merge pull request #7 from hossain-khan/copilot/fix-6
Add kotlinx.serialization support for JSON5 using this library
2 parents 27457f7 + bd4dceb commit 19facf3

File tree

5 files changed

+527
-1
lines changed

5 files changed

+527
-1
lines changed

buildSrc/src/main/kotlin/kotlin-jvm.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ plugins {
1111

1212
kotlin {
1313
// Use a specific Java version to make it easier to work in different environments.
14-
jvmToolchain(23)
14+
jvmToolchain(21)
1515
}
1616

1717
tasks.withType<Test>().configureEach {

lib/src/main/kotlin/io/github/json5/kotlin/JSON5.kt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
package io.github.json5.kotlin
22

3+
import kotlinx.serialization.*
4+
35
/**
46
* JSON5 Implementation for Kotlin
57
*
68
* This is the main entry point for working with JSON5 data.
79
*/
810
object JSON5 {
11+
/**
12+
* Default JSON5 format for kotlinx.serialization.
13+
*/
14+
private val format = DefaultJSON5Format
15+
916
/**
1017
* Parses a JSON5 string into a Kotlin object.
1118
*
@@ -39,4 +46,50 @@ object JSON5 {
3946
fun stringify(value: Any?, space: Any? = null): String {
4047
return JSON5Serializer.stringify(value, space)
4148
}
49+
50+
/**
51+
* Encodes the given [value] to JSON5 string using kotlinx.serialization.
52+
*
53+
* @param serializer The serializer for type [T]
54+
* @param value The value to encode
55+
* @return JSON5 string representation
56+
*
57+
* ```kotlin
58+
* @Serializable
59+
* data class Config(val name: String, val version: Int)
60+
*
61+
* val config = Config("MyApp", 1)
62+
* val json5 = JSON5.encodeToString(Config.serializer(), config)
63+
* // Result: {name:'MyApp',version:1}
64+
* ```
65+
*/
66+
fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
67+
return format.encodeToString(serializer, value)
68+
}
69+
70+
/**
71+
* Decodes the given JSON5 [string] to a value of type [T] using kotlinx.serialization.
72+
*
73+
* @param deserializer The deserializer for type [T]
74+
* @param string JSON5 string to decode
75+
* @return Decoded value of type [T]
76+
*
77+
* ```kotlin
78+
* @Serializable
79+
* data class Config(val name: String, val version: Int)
80+
*
81+
* val json5 = """
82+
* {
83+
* // Application name
84+
* name: 'MyApp',
85+
* version: 1, // current version
86+
* }
87+
* """.trimIndent()
88+
* val config = JSON5.decodeFromString(Config.serializer(), json5)
89+
* // Result: Config(name="MyApp", version=1)
90+
* ```
91+
*/
92+
fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
93+
return format.decodeFromString(deserializer, string)
94+
}
4295
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
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

Comments
 (0)