Continúo con la serie de entradas de la serie sobre la librería Circe. En la presente entrada, Circe III: codificacores y decodificadores, me centraré en los codificadores y decodificadores de Circe los cuáles se definen como type classes.
El sketchnote de la presente entrada, queda descrito en la siguiente imagen:
Un codificador, Encoder[A], contiene una función que convierte un elemento de tipo A en un JSON; y, un decodificador, Decoder[A], realiza la función inversa de un JSON de un tipo A. La librería Circe contiene instancias implícitas de estas type classes para muchos tipos de scala como Int, String, List[A], Option[A] y otros.
Operaciones básicas
Para realizar las operaciones básicas es necesario importar los elementos de los siguientes paquetes:
import io.circe.parser.decode import io.circe.syntax._
- Para codificar una lista en JSON podemos utilizar la función asJson, o bien, as[TipoDato]. Unos ejemplos ilustrativos son los siguientes:
val intsJson = List(1, 2, 3).asJson println(s"[-] Codificación usando la sintaxis: List->JSON=${List(1, 2, 3).asJson}") println(s"[-] Decodificación usando la sintaxis de Json->List[Int]=${intsJson.as[List[Int]]}")
La salida por consola es la siguiente:
[-] Codificación usando la sintaxis: List->JSON=[ 1, 2, 3 ] [-] Decodificación usando la sintaxis de Json->List[Int]=Right(List(1, 2, 3))
- Para decodificar una estructura JSON a un tipo determinado, se emplea la función decode. Unos ejemplos ilustrativos son los siguientes:
println(s"[-] Decodificación String->List[Int]=${decode[List[Int]]("[1, 2, 3]")}") println(s"[-] Decodificación String->Seq[Int]=${decode[Seq[Int]]("[1, 2, 3]")}") println(s"[-] Decodificación String->List[Option[Int]]=${decode[List[Option[Int]]]("[1, 2, 3]")}")
La salida por consola es la siguiente:
[-] Decodificación String->List[Int]=Right(List(1, 2, 3)) [-] Decodificación String->Seq[Int]=Right(List(1, 2, 3)) [-] Decodificación String->List[Option[Int]]=Right(List(Some(1), Some(2), Some(3)))
Operaciones semiautomáticas
En ciertos momentos es necesario tener definidos Encoder y Decoder para una case class determinada. Para ello, utilizamos los componentes semiautomáticos y los importamos de la siguiente manera:
import io.circe._ import io.circe.generic.semiauto._
Definimos una case class con nombre Foo de prueba como sigue:
case class Foo(a: Int, b: String, c: Boolean)
Definimos los codificadores y decodificadores para la case class de forma implícita de la siguiente forma:
implicit val fooDecoder: Decoder[Foo] = deriveDecoder[Foo] implicit val fooEncoder: Encoder[Foo] = deriveEncoder[Foo]
La operación de codificar la case class Foo a un JSON y su proceso contrario es el siguiente:
println(s"[*] Encoder Foo->Json =${fooEncoder(Foo(a = 1, b = "b", c = true))}") val jsonFoo = fooEncoder(Foo(a = 2, b = "bbb", c = false)) val cursor: HCursor = jsonFoo.hcursor println(s"[*] Decoder Json->Foo =${fooDecoder(cursor)}")
La salida por consola es la siguiente:
[*] Encoder Foo->Json ={ "a" : 1, "b" : "b", "c" : true } [*] Decoder Json->Foo =Right(Foo(2,bbb,false))
Operaciones con anotaciones
La librería Circe contiene unas anotaciones que permite realizar operaciones de codificación, permitiendo la codificación de un case class a JSON. La anotación se encuentra definida en el siguiente paquete io.circe.generic.JsonCodec. Así, un ejemplo de codificación de case class a JSON es el siguiente:
import io.circe.syntax._ import io.circe.generic.JsonCodec @JsonCodec case class Bar(i: Int, s: String) println(s"[-] Conversión Bar->JSON=${Bar(i = 1, s = "Prueba").asJson}")
La salida por consola es la siguiente:
[-] Conversión Bar->JSON={ "i" : 1, "s" : "Prueba" }
- Para una relación de clases anidadas, la utilización de anotaciones permite una automatización sencilla y eficiente. Así, dadas las siguientes entidades:
import io.circe.generic.JsonCodec @JsonCodec case class Person2(name: String) @JsonCodec case class Greeting2(salutation: String, person: Person2, exclamationMarks: Int)
- La operación de conversión de una entidad a JSON, se realiza de la siguiente forma:
println(s"[*] CLASE COMPLEJA -> JSON, usando @JsonCodec=${Greeting2("Hey", Person2("Chris"), 3).asJson}")
La salida por consola es la siguiente:
[*] CLASE COMPLEJA -> JSON, usando @JsonCodec={ "salutation" : "Hey", "person" : { "name" : "Chris" }, "exclamationMarks" : 3 }
- La operación de conversión de JSON a la entidad, se realiza de la siguiente forma:
val greetingJSON = """ { "salutation" : "Hey", "person" : { "name" : "Chris" }, "exclamationMarks" : 3 } """ import io.circe.parser.decode println(s"Decodificacion=${decode[Greeting2](greetingJSON)}")
La salida por consola es la siguiente:
Decodificacion=Right(Greeting2(Hey,Person2(Chris),3))
Helper method
En ciertos momentos es necesario tener un codificador y un decodificador para una determinada entidad de negocio. Para ello, definimos un objeto con métodos helper de forma implícita. Así, para la siguiente entidad User definimos la clase codificadora de la siguiente forma:
import io.circe.{Decoder, Encoder} case class User(id: Long, firstName: String, lastName: String) object UserCodec { implicit val decodeUser: Decoder[User] = Decoder.forProduct3("id", "first_name", "last_name")(User.apply) implicit val encodeUser: Encoder[User] = Encoder.forProduct3("id", "first_name", "last_name")(u => (u.id, u.firstName, u.lastName) ) }
En este caso hemos usado la función forProduct3 porque la entidad User tiene tres campos; pero, existen funciones forProductN (siendo N un número natural salvo el cero) para las clases con distinto número de campos. Un ejemplo de utilización de los helper definidos en el objeto UserCodec es el siguiente:
println(s"[--] Codificación User->JSON =${encodeUser(User(id = 1, firstName = "11", lastName = "111"))}") val jsonPrueba = encodeUser(User(id = 69, firstName = "Prueba Decodificación", lastName = "Prueba Decodificación")) val cursor: HCursor = jsonPrueba.hcursor println(s"[--] Decodificación JSON->User =${decodeUser(cursor)}")
La salida por consola es la siguiente:
[--] Codificación User->JSON ={ "id" : 1, "first_name" : "11", "last_name" : "111" } [--] Decodificación JSON->User =Right(User(69,Prueba Decodificación,Prueba Decodificación))
Codificadores y Decodificadores a medida
En Circe existe la posibilidad de definir codificadores y decodificadores a medida. Para esta operación, utilizamos las entidades Encoder y Decoder. Así, para una entidad ejemplo Thing, las clases codificadores y decodificadoras se definen de la siguiente manera:
import io.circe.{ Decoder, Encoder, HCursor, Json } class Thing(val foo: String, val bar: Int) implicit val encodeFoo: Encoder[Thing] = new Encoder[Thing] { final def apply(a: Thing): Json = Json.obj( ("foo", Json.fromString(a.foo)), ("bar", Json.fromInt(a.bar)) ) } implicit val decodeFoo2: Decoder[Thing] = new Decoder[Thing] { final def apply(c: HCursor): Decoder.Result[Thing] = for { foo <- c.downField("foo").as[String].right bar <- c.downField("bar").as[Int].right } yield { new Thing(foo, bar) } }
Para realizar la operación de codificación de un objeto de la clase Thing a un JSON, se realiza de la siguiente manera:
println(s"[A*] Codificador de una clase THING->JSON = ${encodeFoo(new Thing("PruebaCliente",12))} ")
La salida por consola es la siguiente:
[A*] Codificador de una clase THING->JSON = { "foo" : "PruebaCliente", "bar" : 12 }
Para acceder a los valores de los campos definidos en el JSON, se realiza empleando el campo downField. La obtención de los campos del JSON anterior, se realiza de la siguiente forma:
val cursorPruebaCodificada: HCursor = pruebaCodificado.hcursor println(s"[A*] Campo bar = ${cursorPruebaCodificada.downField("bar").as[Int].right } ") println(s"[A*] Campo foo = ${cursorPruebaCodificada.downField("foo").as[String].right } ") println(s"[A*] Campo JSON = ${cursorPruebaCodificada.top } ")
La salida por consola es la siguiente:
[A*] Campo bar = RightProjection(Right(12)) [A*] Campo foo = RightProjection(Right(PruebaCliente)) [A*] Campo JSON = Some({ "foo" : "PruebaCliente", "bar" : 12 })
Para realizar la operación de decodificar un JSON a un objeto Thing, se utiliza un Decoder de la siguiente forma:
println(s"[A*] Decodificador JSON -> Thing = ${decodeFoo2( cursorPruebaCodificada ).right.e.right.get } ")
La salida por consola es la siguiente:
[A*] Decodificador JSON -> Thing = Thing(PruebaCliente,12)
Codificadores de claves
La existencia y sencillez de uso de los codificadores y decodificadores, permite emplear su funcionalidad para otras tareas como las codificaciones de claves. Circe proporciona dos elementos para para la codificación y decodificación de claves, respectivamente KeyEncoder y KeyDecoder.
Sea una clave definida por una clase Foo como sigue:
case class Foo(value: String)
Definimos los codificadores y decodificadores implícitos de la siguiente manera:
implicit val fooKeyEncoder: KeyEncoder[Foo] = new KeyEncoder[Foo] { override def apply(foo: Foo): String = foo.value } implicit val fooKeyDecoder: KeyDecoder[Foo] = new KeyDecoder[Foo] { override def apply(key: String): Option[Foo] = Some(Foo(key)) }
Sea la siguiente estructura Map con un conjunto de claves y valores:
val map = Map[Foo, Int]( Foo("hola") -> 123, Foo("mundo") -> 456 )
El proceso de conversión de la estructura Map en JSON, se realiza de la siguiente forma:
val mapJson= map.asJson println(s"Conversión Map[Foo, Int]-> Json =${mapJson}")
La salida por consola es la siguiente:
Conversión Map[Foo, Int]-> Json ={ "hola" : 123, "mundo" : 456 }
El preceso de conversión de una estructura JSON a una estructura Map, se realiza decodificando los valores de la siguiente forma:
println(s"Conversión Json -> Map[Foo, Int] =${mapJson.as[Map[Foo, Int]]}")
La salida por consola es la siguiente:
Conversión Json -> Map[Foo, Int] =Right(Map(Foo(hola) -> 123, Foo(mundo) -> 456))
En la siguiente entrada, Circe IV: ópticas, realizaré una descripción del uso de ópticas con Circe.