zio-json
zio-json copied to clipboard
Support for enum / sealed traits without hints nor discrimination fields
Taking an enum like this:
enum Credentials:
case UserPassword(username: String, password: String)
case Token(jwt: String)
I need to be able to parse the next 2 jsons:
{"username": "$username", "password": "$password"}
{"jwt": "$token"}
Taking as input the field names of each option we can implement something like this (manual approach):
given credentialsEncoder: JsonEncoder[Credentials] =
(a: Credentials, indent: Option[Int], out: Write) => a match
case Credentials.UserPassword(username, password) =>
out.write(s"""{"username": "$username", "password": "$password"}""")
case Credentials.Token(token) =>
out.write(s"""{"$JWT": "$token"}""")
given credentialsDecoder: JsonDecoder[Credentials] = (trace: List[JsonError], in: RetractReader) =>
val map = JsonDecoder[Map[String, String]].unsafeDecode(trace, in)
if map.contains(JWT) then
Token(map.get(JWT).get)
else if map.contains(USERNAME) && map.contains(PASSWORD) then
UserPassword(map.get(USERNAME).get, map.get(PASSWORD).get)
else
unsafeDecodeMissing(trace)
Which pass the next test:
import scala.concurrent.duration.*
import zio.*
import zio.config.*
import zio.json.*
import zio.test.*
import zio.test.Assertion.*
trait CredentialExamples:
val username = "root"
val password = "testPassword"
val userPasswordJson = s"""{"username": "$username", "password": "$password"}"""
private val token = """eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcmVmZXJyZWRfdXNlcm5hbWUiOiJyb290IiwiaXNzIjoiYXJhbmdvZGIiLCJpYXQiOjE2NjUwNDU2MjAsImV4cCI6MTY2NzYzNzYyMH0.9IZwKAEALrH8iSN_6YHzv4pAM0Y7a-W22mnCz-bvMa0"""
val tokenJson = s"""{"jwt": "$token"}"""
val userPasswordCredentials = Credentials.UserPassword(username, password)
val tokenCredentials = Credentials.Token(token)
object CredentialsSpec extends ZIOSpecDefault with CredentialExamples:
def assertParsedJsonIs[T: JsonDecoder](json: String, expected: T) =
assert(json.fromJson[T])(isRight(equalTo(expected)))
override def spec: Spec[TestEnvironment, Any] =
suite("Credentials should")(
test("encode user password credentials") {
assertTrue(userPasswordCredentials.toJson == userPasswordJson)
},
test("decode user password credentials") {
assertParsedJsonIs(userPasswordJson, userPasswordCredentials)
},
test("encode token credentials") {
assertTrue(tokenCredentials.toJson == tokenJson)
},
test("decode token credentials") {
assertParsedJsonIs(tokenJson, tokenCredentials)
}
)
I think this implementation can be done getting the definition of each case
and trying to match with the json provided.
Something like this on Circe: https://circe.github.io/circe/codecs/adt.html
@carlos-verdes BEWARE: Default implementations for Map
are vulnerable. Possible mitigations are limiting a number of accepted key-value pairs during parsing or using implementations which are safer: TreeMap
, java.util.HashMap
with Map
adapter, etc.
Map is only an example to show the expected output, it's obviously not a good practice to load full document in memory
I end up with something like that (but there are lot of things to improve:
given credentialsDecoder: JsonDecoder[Credentials] = new JsonDecoder[Credentials]:
override def unsafeDecode(trace: List[JsonError], in: RetractReader): Credentials =
Lexer.char(trace, in, '{')
Lexer.firstField(trace, in)
val firstKey = Lexer.string(trace, in).toString
Lexer.char(trace, in, ':')
if firstKey == JWT then Token(Lexer.string(trace, in).toString)
else if firstKey == USERNAME then
val username = Lexer.string(trace, in).toString
if Lexer.nextField(trace, in) && Lexer.string(trace, in).toString == PASSWORD then
Lexer.char(trace, in, ':')
val password = Lexer.string(trace, in).toString
UserPassword(username, password)
else unsafeDecodeMissing(trace)
else if firstKey == PASSWORD then
val password = Lexer.string(trace, in).toString
if Lexer.nextField(trace, in) && Lexer.string(trace, in).toString == USERNAME then
Lexer.char(trace, in, ':')
val username = Lexer.string(trace, in).toString
UserPassword(username, password)
else unsafeDecodeMissing(trace)
else unsafeDecodeMissing(trace)