fix #75 simple classes JSON serialization with custom formats

This commit is contained in:
Sergey Chernov 2025-12-04 23:58:31 +01:00
parent 0c31ec63ee
commit 84f2f8fac4
3 changed files with 173 additions and 63 deletions

View File

@ -1,17 +1,69 @@
# Json support
Since 1.0.5 we start adding JSON support.
Since 1.0.5 we start adding JSON support. Versions 1,0,6* support serialization of the basic types, including lists and maps, and simple classes. Multiple inheritance may produce incorrect results, it is work in progress.
Right now we only support basic types: maps, lists, strings, numbers, booleans. It is not yet capable of serializing classes. This functionality will be added in the 1.0.6 release.
## Serializae to kotlin string
## Serialization in Lyng
// in lyng
assertEquals("{\"a\":1}", {a: 1}.toJsonString())
void
>>> void
From the kotln side, you can use `Obj.toJson()` and deserialization helpers:
Simple classes serialization is supported:
import lyng.serialization
class Point(foo,bar) {
val t = 42
}
// val is not serialized
assertEquals( "{\"foo\":1,\"bar\":2}", Point(1,2).toJsonString() )
>>> void
Note that mutable members are serialized:
import lyng.serialization
class Point2(foo,bar) {
var reason = 42
// but we override json serialization:
fun toJsonObject() {
{ "custom": true }
}
}
// var is serialized instead
assertEquals( "{\"custom\":true}", Point2(1,2).toJsonString() )
>>> void
Custom serialization of user classes is possible by overriding `toJsonObject` method. It must return an object which is serializable to Json. Most often it is a map, but any object is accepted, that makes it very flexible:
import lyng.serialization
class Point2(foo,bar) {
var reason = 42
// but we override json serialization:
fun toJsonObject() {
{ "custom": true }
}
}
class Custom {
fun toJsonObject() {
"full freedom"
}
}
// var is serialized instead
assertEquals( "\"full freedom\"", Custom().toJsonString() )
>>> void
Please note that `toJsonString` should be used to get serialized string representation of the object. Don't call `toJsonObject` directly, it is not intended to be used outside the serialization library.
## Kotlin side interfaces
The "Batteries included" principle is also applied to serialization.
- `Obj.toJson()` provides Kotlin `JsonElement`
- `Obj.toJsonString()` provides Json string representation
- `Obj.decodeSerializableWith()` and `Obj.decodeSerializable()` allows to decode Lyng classes as Kotlin objects using `kotlinx.serialization`:
```kotlin
/**
@ -38,6 +90,6 @@ suspend inline fun <reified T>Obj.decodeSerializable(scope: Scope= Scope()) =
decodeSerializableWith<T>(serializer<T>(), scope)
```
Note that lyng-2-kotlin deserialization with `kotlinx.serialization` is working based on JsonElement as information carrier, without formatting and parsing actual Json strings. This is why we use `Json.decodeFromJsonElement` instead of `Json.decodeFromString`. Such approach gives satisfactory performance without writing and supporting custom `kotlinx.serialization` codecs.
Note that lyng-2-kotlin deserialization with `kotlinx.serialization` uses JsonElement as information carrier without formatting and parsing actual Json strings. This is why we use `Json.decodeFromJsonElement` instead of `Json.decodeFromString`. Such an approach gives satisfactory performance without writing and supporting custom `kotlinx.serialization` codecs.

View File

@ -232,8 +232,11 @@ class ObjInstance(override val objClass: ObjClass) : Obj() {
?: scope.raiseError("can't serialize non-serializable object (no constructor meta)")
for (entry in meta.params)
result[entry.name] = readField(scope, entry.name).value.toJson(scope)
for (i in serializingVars)
result[i.key] = i.value.value.toJson(scope)
for (i in serializingVars) {
// remove T:: prefix from the field name for JSON
val parts = i.key.split("::")
result[if( parts.size == 1 ) parts[0] else parts.last()] = i.value.value.toJson(scope)
}
return JsonObject(result)
}

View File

@ -928,10 +928,10 @@ class ScriptTest {
assertEquals(7, c.eval("x++").toInt())
assertEquals(
8, c.eval("x")
.also {
println("${it.toDouble()} ${it.toInt()} ${it.toLong()} ${it.toInt()}")
}
.toInt())
.also {
println("${it.toDouble()} ${it.toInt()} ${it.toLong()} ${it.toInt()}")
}
.toInt())
}
@Test
@ -1878,7 +1878,7 @@ class ScriptTest {
@Test
fun testIntExponentRealForm() = runTest {
when(val x = eval("1e-6").toString()) {
when (val x = eval("1e-6").toString()) {
"0.000001", "1E-6", "1e-6" -> {}
else -> fail("Excepted 1e-6 got $x")
}
@ -2577,14 +2577,16 @@ class ScriptTest {
@Test
fun testSetAddRemoveSet() = runTest {
eval("""
eval(
"""
val s1 = Set( 1, 2 3)
val s2 = Set( 3, 4 5)
assertEquals( Set(1,2,3,4,5), s1 + s2 )
assertEquals( Set(1,2,3,4,5), s1 + s2.toList() )
assertEquals( Set(1,2), s1 - s2 )
assertEquals( Set(1,2), s1 - s2.toList() )
""".trimIndent())
""".trimIndent()
)
}
@Test
@ -2651,7 +2653,7 @@ class ScriptTest {
)
}
class ObjTestFoo(val value: ObjString): Obj() {
class ObjTestFoo(val value: ObjString) : Obj() {
override val objClass: ObjClass = klass
@ -2669,13 +2671,15 @@ class ScriptTest {
fun TestApplyFromKotlin() = runTest {
val scope = Script.newScope()
scope.addConst("testfoo", ObjTestFoo(ObjString("bar2")))
scope.eval("""
scope.eval(
"""
assertEquals(testfoo.test(), "bar2")
testfoo.apply {
println(test())
assertEquals(test(), "bar2")
}
""".trimIndent())
""".trimIndent()
)
}
@Test
@ -2683,7 +2687,8 @@ class ScriptTest {
withContext(Dispatchers.Default) {
withTimeout(1.seconds) {
val s = Script.newScope()
s.eval("""
s.eval(
"""
fun dosomething() {
var x = 0
for( i in 1..100) {
@ -2692,7 +2697,8 @@ class ScriptTest {
delay(100)
assert(x == 5050)
}
""".trimIndent())
""".trimIndent()
)
(0..100).map {
globalDefer {
s.eval("dosomething()")
@ -2707,7 +2713,8 @@ class ScriptTest {
withContext(Dispatchers.Default) {
withTimeout(1.seconds) {
val s = Script.newScope()
s.eval("""
s.eval(
"""
// it is intentionally not optimal to provoke
// RC errors:
class AtomicCounter {
@ -2742,7 +2749,8 @@ class ScriptTest {
}
assertEquals( 100, ac.getCounter() )
""".trimIndent())
""".trimIndent()
)
}
}
}
@ -3580,7 +3588,7 @@ class ScriptTest {
}
// @Test
// @Test
fun testMinimumOptimization() = runTest {
for (i in 1..200) {
bm {
@ -3627,16 +3635,24 @@ class ScriptTest {
val scope1 = Script.newScope()
// extension foo should be local to scope1
assertEquals("a_foo", scope1.eval("""
assertEquals(
"a_foo", scope1.eval(
"""
fun String.foo() { this + "_foo" }
"a".foo()
""".trimIndent()).toString())
""".trimIndent()
).toString()
)
val scope2 = Script.newScope()
assertEquals("a_bar", scope2.eval("""
assertEquals(
"a_bar", scope2.eval(
"""
fun String.foo() { this + "_bar" }
"a".foo()
""".trimIndent()).toString())
""".trimIndent()
).toString()
)
}
@Test
@ -3658,15 +3674,18 @@ class ScriptTest {
@Test
fun testRangeIsIterable() = runTest {
eval("""
eval(
"""
val r = 1..10
assert( r is Iterable )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testCallAndResultOrder() = runTest {
eval("""
eval(
"""
import lyng.stdlib
fun test(a="a", b="b", c="c") { [a, b, c] }
@ -3681,13 +3700,15 @@ class ScriptTest {
// the parentheses here are in fact unnecessary:
val ok2 = test { void }.last()
assert( ok2 is Callable)
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testIterableMinMax() = runTest {
eval("""
eval(
"""
import lyng.stdlib
assertEquals( -100, (1..100).toList().minOf { -it } )
assertEquals( -1, (1..100).toList().maxOf { -it } )
@ -3764,29 +3785,34 @@ class ScriptTest {
@Test
fun testInlineArrayLiteral() = runTest {
eval("""
eval(
"""
val res = []
for( i in [4,3,1] ) {
res.add(i)
}
assertEquals( [4,3,1], res )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testInlineMapLiteral() = runTest {
eval("""
eval(
"""
val res = {}
for( i in {foo: "bar"} ) {
res[i.key] = i.value
}
assertEquals( {foo: "bar"}, res )
""".trimIndent())
""".trimIndent()
)
}
@Test
fun testCommentsInClassConstructor() = runTest {
eval("""
eval(
"""
class T(
// comment 1
val x: Int,
@ -3801,7 +3827,7 @@ class ScriptTest {
}
@Serializable
data class JSTest1(val foo: String,val one: Int, val ok: Boolean)
data class JSTest1(val foo: String, val one: Int, val ok: Boolean)
@Test
fun testToJson() = runTest {
@ -3810,26 +3836,30 @@ class ScriptTest {
assertEquals(x.toJson().toString(), """{"foo":"bar","one":1,"ok":true}""")
assertEquals(
(eval("""{ "foo": "bar", "one": 1, "ok": true }.toJsonString()""") as ObjString).value,
"""{"foo":"bar","one":1,"ok":true}""")
"""{"foo":"bar","one":1,"ok":true}"""
)
println(x.decodeSerializable<JSTest1>())
assertEquals(JSTest1("bar", 1, true), x.decodeSerializable<JSTest1>())
}
@Test
fun testInstanceVars() = runTest {
var x = eval("""
var x = eval(
"""
// in this case, x and y are constructor parameters, not instance variables:
class T(x,y) {
// and z is val and therefore needn't serialization either:
val z = x + y
}
T(1, 2)
""".trimIndent()) as ObjInstance
println(x.serializingVars.map { "${it.key}=${it.value.value}"})
""".trimIndent()
) as ObjInstance
println(x.serializingVars.map { "${it.key}=${it.value.value}" })
// so serializingVars is empty:
assertEquals(emptyMap(), x.serializingVars)
x = eval("""
x = eval(
"""
class T(x,y) {
// variable z though should be serialized:
var z = x + y
@ -3838,17 +3868,19 @@ class ScriptTest {
x.z = 100
assertEquals(100, x.z)
x
""".trimIndent()) as ObjInstance
""".trimIndent()
) as ObjInstance
// z is instance var, it must present
val z = x.serializingVars["z"] ?: x.serializingVars["T::z"]
// and be mutable:
assertTrue( z!!.isMutable )
println(x.serializingVars.map { "${it.key}=${it.value.value}"})
assertTrue(z!!.isMutable)
println(x.serializingVars.map { "${it.key}=${it.value.value}" })
}
@Test
fun memberValCantBeAssigned() = runTest {
eval("""
eval(
"""
class Point(foo,bar) {
val t = 42
var r = 142
@ -3867,23 +3899,46 @@ class ScriptTest {
// but r should be changed:
assertEqual(123, p.r)
""")
"""
)
}
// @Test
// fun testClassToJson() = runTest {
// val x = eval("""
// import lyng.serialization
// class Point(foo,bar) {
// val t = 42
// }
// val p = Point(1,2)
// p.t = 121
// println(Point(10,"bar").toJsonString())
// Lynon.encode(Point(1,2))
// """.trimIndent())
// println((x as ObjBitBuffer).bitArray.asUByteArray().toDump())
//
// }
@Test
fun testClassToJson() = runTest {
eval(
"""
import lyng.serialization
class Point(foo,bar) {
val t = 42
}
// val is not serialized
assertEquals( "{\"foo\":1,\"bar\":2}", Point(1,2).toJsonString() )
class Point2(foo,bar) {
var reason = 42
}
// var is serialized instead
assertEquals( "{\"foo\":1,\"bar\":2,\"reason\":42}", Point2(1,2).toJsonString() )
""".trimIndent()
)
}
@Test
fun testCustomClassToJson() = runTest {
eval(
"""
import lyng.serialization
class Point2(foo,bar) {
var reason = 42
// but we override json serialization:
fun toJsonObject() {
{ "custom": true }
}
}
// var is serialized instead
assertEquals( "{\"custom\":true}", Point2(1,2).toJsonString() )
""".trimIndent()
)
}
}