I am struggling to understand how to parse an empty object {} with the experimental kotlinx.serialization library. The complication arises when in fact an API response can be one of;
{
"id": "ABC1",
"status": "A_STATUS"
}
or
{}
The data structure I have used as my serializer is;
data class Thing(val id: String = "", val status: String = "")
This is annotated with #kotlinx.serialization.Serializable and used within an API client library to marshall between the raw API response and the data model. The default values tell the serialisation library that the field is optional and replaces the #Optional approach of pre-Kotlin 1.3.30.
Finally, the kotlinx.serialization.json.Json parser I am using has the configuration applied by using the nonstrict template.
How do I define a serializer that can parse both an empty object and the expected data type with kotlinx.serialization? Do I need to write my own KSerialiser or is there config I am missing. Ideally, the empty object should be ignored/parsed as a null?
The error I get when parsing an empty object with my Thing data class is;
Field 'id' is required, but it was missing
So this was down to the kotlinCompilerClasspath having a different version kotlin (1.3.21, not 1.3.31).
Interestingly this was owing to advice I followed when configuring my gradle plugin project to not specify a version for the kotlin-dsl plugin.
Explicitly relying on the version I needed fixed the kotlinx.serialisation behavior (no changes to the mainline code)
Yes, ideally null instead of {} is way more convenient to parse but sometimes you just need to consume what backend sends you
There are 2 solutions that come to my mind.
Simpler, specific to your case using map:
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class ThingMapSerializerTest {
#Test
fun `should deserialize to non empty map`() {
val thingMap: Map<String, String> =
Json.decodeFromString("""{"id":"ABC1","status":"A_STATUS"}""")
assertTrue(thingMap.isNotEmpty())
assertEquals("ABC1", thingMap["id"])
assertEquals("A_STATUS", thingMap["status"])
}
#Test
fun `should deserialize to empty map`() {
val thingMap: Map<String, String> = Json.decodeFromString("{}")
assertTrue(thingMap.isEmpty())
}
}
More complex but more general that works for any combinations of value types. I recommend sealed class with explicit empty value instead of data class with empty defaults:
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.serialDescriptor
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.json.Json
import org.junit.Assert.assertEquals
import org.junit.Test
class ThingSerializerTest {
#Test
fun `should deserialize to thing`() {
val thing: OptionalThing =
Json.decodeFromString(
OptionalThing.ThingSerializer,
"""{"id":"ABC1","status":"A_STATUS"}"""
)
assertEquals(OptionalThing.Thing(id = "ABC1", status = "A_STATUS"), thing)
}
#Test
fun `should deserialize to empty`() {
val thing: OptionalThing =
Json.decodeFromString(OptionalThing.ThingSerializer, "{}")
assertEquals(OptionalThing.Empty, thing)
}
sealed class OptionalThing {
data class Thing(val id: String = "", val status: String = "") : OptionalThing()
object Empty : OptionalThing()
object ThingSerializer : KSerializer<OptionalThing> {
override val descriptor: SerialDescriptor =
buildClassSerialDescriptor("your.app.package.OptionalThing") {
element("id", serialDescriptor<String>(), isOptional = true)
element("status", serialDescriptor<String>(), isOptional = true)
}
override fun deserialize(decoder: Decoder): OptionalThing {
decoder.decodeStructure(descriptor) {
var id: String? = null
var status: String? = null
loop# while (true) {
when (val index = decodeElementIndex(descriptor)) {
CompositeDecoder.DECODE_DONE -> break#loop
0 -> id = decodeStringElement(descriptor, index = 0)
1 -> status = decodeStringElement(descriptor, index = 1)
else -> throw SerializationException("Unexpected index $index")
}
}
return if (id != null && status != null) Thing(id, status)
else Empty
}
}
override fun serialize(encoder: Encoder, value: OptionalThing) {
TODO("Not implemented, not needed")
}
}
}
}
When 'Thing' is a field within json object:
"thing":{"id":"ABC1","status":"A_STATUS"} // could be {}
you can annotate property like that:
#Serializable(with = OptionalThing.ThingSerializer::class)
val thing: OptionalThing
Tested for:
classpath "org.jetbrains.kotlin:kotlin-serialization:1.4.10"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1"
Related
I have a Kotlin server that acts as a gateway, handling communication between a client and a number of backing services over a REST APIs, using JSON. My server uses Kotlinx Serialization for serialization.
Usually I need to parse and adapt the responses from the backing services, but occasionally I just want to return the raw JSON content as a response.
For example:
import kotlinx.serialization.json.*
fun main() {
// I get some JSON from a backing service
val backingServiceResponse = """
{"some":"json",id:123,content:[]}
""".trimIndent()
// I create a response object, that I will return to the client
val packet = ExampleClientResponse("name", backingServiceResponse)
val encodedPacket = Json.encodeToString(packet)
println(encodedPacket)
// I expect that the JSON is encoded without quotes
require("""{"name":"name","content":{"some":"json",id:123,content:[]}}""" == encodedPacket)
}
#Serializable
data class ExampleClientResponse(
val name: String,
val content: String, // I want this to be encoded as unescaped JSON
)
However, the value of content is surrounded by quotes, and is escaped
{
"name":"name",
"content":"{\"some\":\"json\",id:123,content:[]}"
}
What I want is for the content property to be literally encoded:
{
"name":"name",
"content":{
"some":"json",
"id":123,
"content":[]
}
}
I am using Kotlin 1.8.0 and Kotlinx Serialization 1.4.1.
Encoding raw JSON is possible in Kotlinx Serialization 1.5.0-RC, which was released on 26th Jan 2023, and is experimental. It is not possible in earlier versions.
Create a custom serializer
First, create a custom serializer, RawJsonStringSerializer, that will encode/decode strings.
Encoding needs to use the new JsonUnquotedLiteral function to encode the content, if we're encoding the string as JSON
Since the value being decoded might be a JSON object, array, or primitive, it must use JsonDecoder, which has the decodeJsonElement() function. This will dynamically decode whatever JSON data is present to a JsonElement, which can be simply be converted to a JSON string using toString().
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*
private object RawJsonStringSerializer : KSerializer<String> {
override val descriptor = PrimitiveSerialDescriptor("my.project.RawJsonString", PrimitiveKind.STRING)
/**
* Encodes [value] using [JsonUnquotedLiteral], if [encoder] is a [JsonEncoder],
* or with [Encoder.encodeString] otherwise.
*/
#OptIn(ExperimentalSerializationApi::class)
override fun serialize(encoder: Encoder, value: String) = when (encoder) {
is JsonEncoder -> encoder.encodeJsonElement(JsonUnquotedLiteral(value))
else -> encoder.encodeString(value)
}
/**
* If [decoder] is a [JsonDecoder], decodes a [kotlinx.serialization.json.JsonElement] (which could be an object,
* array, or primitive) as a string.
*
* Otherwise, decode a string using [Decoder.decodeString].
*/
override fun deserialize(decoder: Decoder): String = when (decoder) {
is JsonDecoder -> decoder.decodeJsonElement().toString()
else -> decoder.decodeString()
}
}
Apply the custom serializer
Now in your class, you can annotated content with #Serializable(with = ...) to use the new serializer.
import kotlinx.serialization.*
#Serializable
data class ExampleClientResponse(
val name: String,
#Serializable(with = RawJsonStringSerializer::class)
val content: String,
)
Result
Nothing has changed in the main method - Kotlinx Serialization will automatically encode content literally, so it will now succeed.
import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.*
import kotlinx.serialization.json.*
fun main() {
val backingServiceResponse = """
{"some":"json",id:123,content:[]}
""".trimIndent()
val packet = ExampleClientResponse("name", backingServiceResponse)
val encodedPacket = Json.encodeToString(packet)
println(encodedPacket)
require("""{"name":"name","content":{"some":"json",id:123,content:[]}}""" == encodedPacket)
}
Reducing duplication
Using #Serializable(with = ...) is simple when it's a on-off usage, but what if you have lots of properties that you want to encode as literal JSON?
Typealias serialization
When a fix is released in Kotlin 1.8.20 this will be possible with a one-liner
// awaiting fix https://github.com/Kotlin/kotlinx.serialization/issues/2083
typealias RawJsonString = #Serializable(with = RawJsonStringSerializer::class) String
#Serializable
data class ExampleClientResponse(
val name: String,
val content: RawJsonString, // will be encoded literally, without escaping
)
Raw encode a value class
Until Kotlinx Serialization can handle encoding typealias-primitives, you can use an inline value class, which we tell Kotlinx Serialization to encode using RawJsonStringSerializer.
#JvmInline
#Serializable(with = RawJsonStringSerializer::class)
value class RawJsonString(val content: String) : CharSequence by content
Now now annotation is needed in the data class:
#Serializable
data class ExampleClientResponse(
val name: String,
val content: RawJsonString, // will be encoded as a literal JSON string
)
RawJsonStringSerializer needs to be updated to wrap/unwrap the value class
#OptIn(ExperimentalSerializationApi::class)
private object RawJsonStringSerializer : KSerializer<RawJsonString> {
override val descriptor = PrimitiveSerialDescriptor("my.project.RawJsonString", PrimitiveKind.STRING)
override fun deserialize(decoder: Decoder): RawJsonString = RawJsonString(decoder.decodeString())
override fun serialize(encoder: Encoder, value: RawJsonString) = when (encoder) {
is JsonEncoder -> encoder.encodeJsonElement(JsonUnquotedLiteral(value.content))
else -> encoder.encodeString(value.content)
}
}
The tradeoff is that it's a little clunky to convert to/from the new value class.
val backingServiceResponse = """
{"some":"json",id:123,content:[]}
""".trimIndent()
// need to wrap backingServiceResponse in the RawJsonString value class
val packet = ExampleClientResponse("name", RawJsonString(backingServiceResponse))
This answer was written using
Kotlin 1.8
Kotlinx Serialization 1.5.0-RC.
I am trying to upgrade from Jackson-2.5 to 2.10
Below deserialization code was working for me, before but post upgrade the solution is failing with following error:
[ Cannot construct instance of `$line11.$read$$iw$$iw$RegularMetric$1` (although at least one Creator exists): non-static inner classes like this can only by instantiated using default, no-argument constructor
at [Source: (String)".... (through reference chain: com.fasterxml.jackson.module.scala.deser.GenericFactoryDeserializerResolver$BuilderWrapper[0]) ]
metricsJson is a valid Json which was working fine before.
Adding code snippet for reference.
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
case class ThresholdExpression(#JsonProperty("term") term: String,
#JsonProperty("comparisonOperator") comparisonOperator: String,
#JsonProperty("thresholdValue") thresholdValue: String,
#JsonProperty("prorated") prorated: Boolean
){
def getThreshold(proratedFactor : Double = 1): String = {
..
..
}
}
case class RegularMetric(#JsonProperty("name") name: String,
#JsonProperty("premiumPlusThreshold") premiumPlusThreshold: List[ThresholdExpression],
#JsonProperty("premiumThreshold") premiumThreshold: List[ThresholdExpression],
#JsonProperty("standardThreshold") standardThreshold: List[ThresholdExpression]){
def getMetricTierColumnName() : String = {
s"${name}Metric"
}
def getMetricRateColumnName() : String = {
s"${name}"
}
}
object Classification {
private def getRegularMetrics(metricsConfigJson: String): List[RegularMetric] = {
JsonUtils.fromJson[List[RegularMetric]](metricsJson)
}
object JsonUtils {
def fromJson[T](json: String)(implicit m : Manifest[T]): T = {
val mapper = new ObjectMapper() with ScalaObjectMapper
mapper.registerModule(DefaultScalaModule)
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
mapper.readValue[T](json)
}
}
}
On deep dive I found that with Jackson-databind 2.10 there are some tight type check implemented because of which my complex object, which consists of lists is not being deserialized, but the error is a bit misleading and I am not able to figure what am I doing wrong here.
As part of one of my courses, I am using an API in Android Studio for the first time. For this I use OkHttp. However, I have the following error when I launch my application. I guess it's because the JSON is not parsed well but I can't find a solution.
If anyone could help me that would be great!
Thank you
The error screenshot
MyMarqueRecyclerViewAdapter.kt
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import com.google.gson.GsonBuilder
import kotlinx.android.synthetic.main.fragment_marque_list.*
import okhttp3.*
import java.io.IOException
/**
* A fragment representing a list of Items.
*/
class MarqueFragment : Fragment(), OnMarqueClickListener {
private var columnCount = 1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
columnCount = it.getInt(ARG_COLUMN_COUNT)
}
fetchJson()
}
fun fetchJson() {
val url = "https://tp3.infomobile.app/api/v1/brand"
val request = Request.Builder().url(url).build()
val lesmarques = OkHttpClient()
lesmarques.newCall(request).enqueue(object: Callback {
override fun onResponse(call: Call, response: Response) {
val body = response.body?.string()
println(body)
val gson = GsonBuilder().create()
val homeFeed = gson.fromJson(body, HomeFeed::class.java)
println(homeFeed)
activity?.runOnUiThread {
recyclerView_main.adapter = MyMarqueRecyclerViewAdapter(homeFeed)
}
}
override fun onFailure(call: Call, e: IOException) {
println("Failed to execute request")
}
})
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_marque_list, container, false)
// Set the adapter
if (view is RecyclerView) {
with(view) {
layoutManager = when {
columnCount <= 1 -> LinearLayoutManager(context)
else -> GridLayoutManager(context, columnCount)
}
//adapter = MyMarqueRecyclerViewAdapter(homeFeed)
}
}
return view
}
override fun onMarqueItemClicked(position: Int) {
Toast.makeText(this.context, "ça marche", Toast.LENGTH_LONG).show()
val intent = Intent(this#MarqueFragment.requireContext(),MainActivity2::class.java)
startActivity(intent)
}
companion object {
// TODO: Customize parameter argument names
const val ARG_COLUMN_COUNT = "column-count"
// TODO: Customize parameter initialization
#JvmStatic
fun newInstance(columnCount: Int) =
MarqueFragment().apply {
arguments = Bundle().apply {
putInt(ARG_COLUMN_COUNT, columnCount)
}
}
}
}
class HomeFeed(val marques: List<Marque>)
class Marque(val id: Int, val name: String)
MarqueFragment.kt
import android.content.Intent
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import ca.ulaval.ima.tp3.placeholder.PlaceholderContent.PlaceholderItem
import ca.ulaval.ima.tp3.databinding.FragmentMarqueBinding
/**
* [RecyclerView.Adapter] that can display a [PlaceholderItem].
* TODO: Replace the implementation with code for your data type.
*/
class MyMarqueRecyclerViewAdapter(val homeFeed: HomeFeed?) : RecyclerView.Adapter<MyMarqueRecyclerViewAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
FragmentMarqueBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = homeFeed?.marques?.get(position)
holder.contentView.text = item.toString()
holder.contentView.setOnClickListener{
val context=holder.contentView.context
val intent = Intent( context, MainActivity2::class.java)
intent.putExtra("marque", item.toString())
context.startActivity(intent)
}
}
override fun getItemCount(): Int {
return homeFeed?.marques!!?.count()
}
inner class ViewHolder(binding: FragmentMarqueBinding) : RecyclerView.ViewHolder(binding.root) {
val contentView: TextView = binding.content
override fun toString(): String {
return super.toString() + " '" + contentView.text + "'"
}
}
}
First of all, you can trace the crash using the log which clearly states that the crash happened at line 42 in MyMarqueRecyclerViewAdapter.kt, in the getItemCount function, which means that this functions is causing it:
override fun getItemCount(): Int {
return homeFeed?.marques!!?.count()
}
What's happening here is that you're forcing kotlin to treat a property that might be null as a non null property, by using the "!!" operator, this tells kotlin that this propery would never be null in any case, and that isn't true in your case since its throwing a NullPointerException, what you actually need to do is allow it to be null and provide an alternative value in case it was null, using the elvis operator, like this:
override fun getItemCount(): Int {
return homeFeed?.marques?.count() ?: 0
}
Another thing is that you're using the count() function, i think you meant to use the size property instead:
override fun getItemCount(): Int {
return homeFeed?.marques?.size ?: 0
}
Second, i noticed your class declaration and compared the content to the json's response content, it seems that the json response conatins a parameter called "content", while the propery in the HomeFeed class is called "marques", which results in GSON not knowing where to get "marques" from, it only knows that there is a parameter called "content" in the json and it doesn't know what to do with it, the best solution would be to annotate the HomeFeed.marques property with a #SerializedName annotation, and provide the corresponding json parameter that should be mapped to this property, like so:
class HomeFeed(
#SerializedName("content")
val marques: List<Marque>
)
class Marque(
#SerializedName("id")
val id: Int,
#SerializedName("name")
val name: String
)
Hope this helps!
Some resources:
Null Safety | Kotlin
Difference between list.count() and list.size
Parsing between Kotlin classes and Json objects using GSON
I have my code structure like this:
File 1:
abstract class SomeClass {
abstract fun print()
companion object {
val versions = arrayOf(ClassV1::class, ClassV2::class)
}
}
#Serializable
data class ClassV1(val x: Int) : SomeClass() {
override fun print() {
println("Hello")
}
}
#Serializable
data class ClassV2(val y: String) : SomeClass() {
override fun print() {
println("World")
}
}
File 2:
fun <T : SomeClass> getSomeObject(json: String, kClass: KClass<T>): SomeClass {
return Json.decodeFromString(json)
}
fun printData(version: Int, json: String) {
val someClass: SomeClass = getSomeObject(json, SomeClass.versions[version])
someClass.print()
}
I have a json in printData that is a serialized form of some sub-class of SomeClass. I also have a version which is used to determine which class structure does the json represent. Based on the version, I want to de-serialize my json string to the appropriate sub-class of SomeClass.
Right now the getSomeObject function deserializes the json to SomeClass (which crashes, as expected). I want to know if there is a way I can deserialize it to the provided KClass.
I know I can do this like below:
val someClass = when (version) {
0 -> Json.decodeFromString<ClassV1>(json)
else -> Json.decodeFromString<ClassV2>(json)
}
But I am trying to avoid this since I can have a lot of such versions. Is there a better way possible?
It seems to me that the following is what you are looking for:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "version",
visible = false)
#JsonSubTypes(
JsonSubTypes.Type(value = ClassV1::class, name = "V1"),
JsonSubTypes.Type(value = ClassV2::class, name = "V2"))
abstract class SomeClass {
(...)
}
This basically means that your JSON would be deserialized as ClassV1 or ClassV2 based on the JSON property version:
V1 would mean that ClassV1 is the target class;
V2 would mean that ClassV2 is the target class.
You can find more information about this at the following online resources:
https://fasterxml.github.io/jackson-annotations/javadoc/2.4/com/fasterxml/jackson/annotation/JsonTypeInfo.html
https://fasterxml.github.io/jackson-annotations/javadoc/2.5/com/fasterxml/jackson/annotation/JsonSubTypes.Type.html
https://www.baeldung.com/jackson-annotations#jackson-polymorphic-type-handling-annotations
Is there an easy way to serialize to json without "tpe" field inside an object?
I need to serialize case classes to json structures and then, send them over the wire (They won't been deserialized in future). I have a specific api, so.. I don't need additional fields.
For class Person illustrated below:
case class Person(Name: String, Age: int, CandyLover: Boolean)
I'd like to see following:
{
"Name": "Paul",
"Age": 23,
"CandyLover": true
}
I would suggest you to take a look at spray-json or argonaut. Or play-json if you are already using Play.
Scala pickling is not really a json library, it was not created to generate JSON for public API, it's not flexible, you can't define JSON protocol first and then provide pickling serialization to match protocol. Pickling is for computer-to-computer serialization where no one is really care about bytes going between apps.
Probably the simplest way to do it is to pass somehow the Hint into JSONPickleBuilder. Quick way to do it is to override builders in JSONPickleFormat by calling hintStaticallyElidedType() method from PickleTools.
Here is kind of code snippet to demonstrate it:
import org.specs2.matcher.JsonMatchers
import org.specs2.mutable.Specification
import org.specs2.specification.Scope
import scala.pickling.Defaults._
import scala.pickling.json.{JSONPickleBuilder, JSONPickleFormat, JsonFormats}
import scala.pickling.{Output, PBuilder, PickleTools, StringOutput}
class PersonJsonFormatsTest extends Specification with JsonMatchers {
trait Context extends Scope with PersonJsonFormats
trait PersonJsonFormats extends JsonFormats {
override implicit val pickleFormat: JSONPickleFormat = new PersonJSONPickleFormat
}
class PersonJSONPickleFormat extends JSONPickleFormat {
override def createBuilder() = new JSONPickleBuilder(this, new StringOutput) with PickleTools {
hintStaticallyElidedType()
}
override def createBuilder(out: Output[String]): PBuilder = new JSONPickleBuilder(this, out) with PickleTools {
hintStaticallyElidedType()
}
}
"Pickle" should {
"Serialize Person without $type field in resulting JSON" in new Context {
case class Person(Name: String, Age: Int, CandyLover: Boolean)
val pickledPersonObject = Person("Paul", 23, CandyLover = true).pickle
println(pickledPersonObject.value)
pickledPersonObject.value must not */("$type" → ".*".r)
}
}
}
The output of println(pickledPersonObject.value) will be as you need:
{
"Name": "Paul",
"Age": 23,
"CandyLover": true
}
This way you will stay aligned with further pickle updates.
P.S. If someone knows more elegant and convenient way to reach the same behaviour - please let us know :-)
For scala-pickling 0.10.x:
import scala.pickling._
import scala.pickling.json.{JSONPickle, JSONPickleBuilder, JSONPickleFormat, JsonFormats}
import scala.pickling.pickler.AllPicklers
object serialization extends JsonFormats with Ops with AllPicklers {
override implicit val pickleFormat: JSONPickleFormat = new JSONPickleFormat {
private def setHints(h: Hintable): Unit = {
h.hintStaticallyElidedType()
h.hintDynamicallyElidedType()
}
override def createBuilder(): JSONPickleBuilder = {
val builder = super.createBuilder()
setHints(builder)
builder
}
override def createBuilder(out: Output[String]): PBuilder = {
val builder = super.createBuilder(out)
setHints(builder)
builder
}
override def createReader(pickle: JSONPickle): PReader = {
val reader = super.createReader(pickle)
setHints(reader)
reader
}
}
}
object SerializationTest extends App {
import serialization._
case class Person(firstName: String, lastName: String)
val pickle: JSONPickle = Person("Evelyn", "Patterson").pickle
val jsonString: String = pickle.value // {"firstName": "Evelyn","lastName": "Patterson"}
val person: Person = jsonString.unpickle[Person]
}
For scala-pickling 0.11.x:
import scala.pickling._
import scala.pickling.json.{JSONPickle, JSONPickleBuilder, JSONPickleFormat}
import scala.pickling.pickler.AllPicklers
object serialization extends AllPicklers {
private final class CustomJSONPickleFormat(tag: FastTypeTag[_]) extends JSONPickleFormat {
private def setHints(h: Hintable) {
h.hintElidedType(tag)
}
override def createBuilder(): JSONPickleBuilder = {
val b = super.createBuilder()
setHints(b)
b
}
override def createBuilder(out: Output[String]): PBuilder = {
val b = super.createBuilder(out)
setHints(b)
b
}
override def createReader(pickle: JSONPickle): PReader = {
val b = super.createReader(pickle)
setHints(b)
b
}
}
implicit val staticOnly = static.StaticOnly // for compile time serialization methods generation
implicit final class EncodeDecodeOps[T](picklee: T) {
def encode(implicit pickler: Pickler[T]): String = {
val pickleFormat = new CustomJSONPickleFormat(pickler.tag)
functions.pickle(picklee)(pickleFormat, pickler).value
}
def decode[A](implicit c: T => String, unpickler: Unpickler[A]): A = {
val pickleFormat = new CustomJSONPickleFormat(unpickler.tag)
functions.unpickle[A](json.JSONPickle(picklee))(unpickler, pickleFormat)
}
}
}
case class Person(firstName: String, lastName: String) {
#transient var x = "test"
}
object SerializationTest extends App {
import serialization._
val jsonString = Person("Lisa", "Daniels").encode
println(jsonString)
val person = jsonString.decode[Person]
println(person)
}