Skip to content

Commit bb5dbed

Browse files
authored
More efficient JSON decoders and encoders for records (#762)
* More efficient JSON decoders and encoders for Schema.GenericRecord * Fix compilation error with Scala.js * Optimize ListMap building for Scala 2.13+ * Code clean up * Code clean up
1 parent 23febad commit bb5dbed

File tree

1 file changed

+123
-117
lines changed

1 file changed

+123
-117
lines changed

zio-schema-json/shared/src/main/scala/zio/schema/codec/JsonCodec.scala

Lines changed: 123 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package zio.schema.codec
22

33
import java.nio.CharBuffer
44
import java.nio.charset.StandardCharsets
5+
import java.util
56
import java.util.concurrent.ConcurrentHashMap
67

78
import scala.annotation.{ switch, tailrec }
@@ -19,6 +20,7 @@ import zio.json.{
1920
JsonFieldEncoder
2021
}
2122
import zio.prelude.NonEmptyMap
23+
import zio.schema.Schema.GenericRecord
2224
import zio.schema._
2325
import zio.schema.annotation.{ rejectExtraFields, _ }
2426
import zio.schema.codec.DecodeError.ReadError
@@ -338,7 +340,7 @@ object JsonCodec {
338340
case Schema.Tuple2(l, r, _) => ZJsonEncoder.tuple2(schemaEncoder(l, cfg, discriminatorTuple), schemaEncoder(r, cfg, discriminatorTuple))
339341
case Schema.Optional(schema, _) => ZJsonEncoder.option(schemaEncoder(schema, cfg, discriminatorTuple))
340342
case Schema.Fail(_, _) => unitEncoder.contramap(_ => ())
341-
case Schema.GenericRecord(_, structure, _) => recordEncoder(structure.toChunk, cfg)
343+
case s @ Schema.GenericRecord(_, _, _) => recordEncoder(s, cfg)
342344
case Schema.Either(left, right, _) => ZJsonEncoder.either(schemaEncoder(left, cfg, discriminatorTuple), schemaEncoder(right, cfg, discriminatorTuple))
343345
case Schema.Fallback(left, right, _, _) => fallbackEncoder(schemaEncoder(left, cfg, discriminatorTuple), schemaEncoder(right, cfg, discriminatorTuple))
344346
case l @ Schema.Lazy(_) => ZJsonEncoder.suspend(schemaEncoder(l.schema, cfg, discriminatorTuple))
@@ -448,14 +450,12 @@ object JsonCodec {
448450
var first = true
449451
values.foreach {
450452
case (key, value) =>
451-
if (first)
452-
first = false
453+
if (first) first = false
453454
else {
454455
out.write(',')
455-
if (indent.isDefined)
456-
ZJsonEncoder.pad(indent_, out)
456+
if (indent.isDefined) pad(indent_, out)
457457
}
458-
string.encoder.unsafeEncode(JsonFieldEncoder.string.unsafeEncodeField(key), indent_, out)
458+
string.encoder.unsafeEncode(key, indent_, out)
459459
if (indent.isEmpty) out.write(':')
460460
else out.write(" : ")
461461
directEncoder.unsafeEncode(value, indent_, out)
@@ -561,7 +561,7 @@ object JsonCodec {
561561
pad(indent_, out)
562562

563563
if (discriminatorChunk.isEmpty && !noDiscriminators) {
564-
string.encoder.unsafeEncode(JsonFieldEncoder.string.unsafeEncodeField(caseName), indent_, out)
564+
string.encoder.unsafeEncode(caseName, indent_, out)
565565
if (indent.isEmpty) out.write(':')
566566
else out.write(" : ")
567567
}
@@ -598,36 +598,44 @@ object JsonCodec {
598598
}
599599
}
600600

601-
private def recordEncoder[Z](structure: Seq[Schema.Field[Z, _]], cfg: Config): ZJsonEncoder[ListMap[String, _]] = {
602-
(value: ListMap[String, _], indent: Option[Int], out: Write) =>
601+
private def recordEncoder(schema: Schema.GenericRecord, cfg: Config): ZJsonEncoder[ListMap[String, _]] = {
602+
val nonTransientFields = schema.nonTransientFields.toArray
603+
val encoders = nonTransientFields.map(field => schemaEncoder(field.schema.asInstanceOf[Schema[Any]], cfg))
604+
if (nonTransientFields.isEmpty) { (_: ListMap[String, _], _: Option[Int], out: Write) =>
605+
out.write("{}")
606+
} else { (value: ListMap[String, _], indent: Option[Int], out: Write) =>
603607
{
604-
if (structure.isEmpty) {
605-
out.write("{}")
606-
} else {
607-
out.write('{')
608-
val indent_ = bump(indent)
608+
out.write('{')
609+
val doPrettyPrint = indent ne None
610+
var indent_ = indent
611+
if (doPrettyPrint) {
612+
indent_ = bump(indent)
609613
pad(indent_, out)
610-
var first = true
611-
structure.foreach {
612-
case field if field.transient || isEmptyOptionalValue(field, value(field.fieldName), cfg) => ()
613-
case f @ Schema.Field(_, a, _, _, _, _) =>
614-
val enc = schemaEncoder(a.asInstanceOf[Schema[Any]], cfg)
615-
if (first)
616-
first = false
617-
else {
618-
out.write(',')
619-
if (indent.isDefined)
620-
ZJsonEncoder.pad(indent_, out)
621-
}
622-
string.encoder.unsafeEncode(JsonFieldEncoder.string.unsafeEncodeField(f.fieldName), indent_, out)
623-
if (indent.isEmpty) out.write(':')
624-
else out.write(" : ")
625-
enc.unsafeEncode(value(f.fieldName), indent_, out)
614+
}
615+
val strEnc = string.encoder
616+
var first = true
617+
var i = 0
618+
while (i < nonTransientFields.length) {
619+
val field = nonTransientFields(i)
620+
val fieldName = field.fieldName
621+
val fieldValue = value(fieldName)
622+
if (!isEmptyOptionalValue(field, fieldValue, cfg)) {
623+
if (first) first = false
624+
else {
625+
out.write(',')
626+
if (doPrettyPrint) pad(indent_, out)
627+
}
628+
strEnc.unsafeEncode(fieldName, indent_, out)
629+
if (doPrettyPrint) out.write(" : ")
630+
else out.write(':')
631+
encoders(i).unsafeEncode(fieldValue, indent_, out)
626632
}
627-
pad(indent, out)
628-
out.write('}')
633+
i += 1
629634
}
635+
if (doPrettyPrint) pad(indent, out)
636+
out.write('}')
630637
}
638+
}
631639
}
632640
}
633641

@@ -722,7 +730,7 @@ object JsonCodec {
722730
case Schema.NonEmptyMap(ks, vs, _) => mapDecoder(ks, vs).mapOrFail(m => NonEmptyMap.fromMapOption(m).toRight("NonEmptyMap expected"))
723731
case Schema.Set(s, _) => ZJsonDecoder.chunk(schemaDecoder(s, -1)).map(entries => entries.toSet)
724732
case Schema.Fail(message, _) => failDecoder(message)
725-
case Schema.GenericRecord(_, structure, _) => recordDecoder(structure.toChunk, schema.annotations.contains(rejectExtraFields()))
733+
case s @ Schema.GenericRecord(_, _, _) => recordDecoder(s)
726734
case Schema.Either(left, right, _) => ZJsonDecoder.either(schemaDecoder(left, -1), schemaDecoder(right, -1))
727735
case s @ Schema.Fallback(_, _, _, _) => fallbackDecoder(s)
728736
case l @ Schema.Lazy(_) => ZJsonDecoder.suspend(schemaDecoder(l.schema, discriminator))
@@ -977,47 +985,46 @@ object JsonCodec {
977985
private def deAliasCaseName(alias: String, caseNameAliases: Map[String, String]): String =
978986
caseNameAliases.getOrElse(alias, alias)
979987

980-
private def recordDecoder[Z](
981-
structure: Seq[Schema.Field[Z, _]],
982-
rejectAdditionalFields: Boolean
983-
): ZJsonDecoder[ListMap[String, Any]] = { (trace: List[JsonError], in: RetractReader) =>
984-
{
985-
val builder: ChunkBuilder[(String, Any)] = zio.ChunkBuilder.make[(String, Any)](structure.size)
988+
private def recordDecoder(schema: GenericRecord): ZJsonDecoder[ListMap[String, Any]] = {
989+
val capacity = schema.fields.size * 2
990+
val spansWithDecoders =
991+
new util.HashMap[String, (JsonError.ObjectAccess, ZJsonDecoder[Any])](capacity)
992+
val defaults = new util.HashMap[String, Any](capacity)
993+
schema.fields.foreach { field =>
994+
val fieldName = field.fieldName
995+
val spanWithDecoder =
996+
(JsonError.ObjectAccess(fieldName), schemaDecoder(field.schema).asInstanceOf[ZJsonDecoder[Any]])
997+
field.nameAndAliases.foreach(x => spansWithDecoders.put(x, spanWithDecoder))
998+
if (field.optional && field.defaultValue.isDefined) defaults.put(fieldName, field.defaultValue.get)
999+
}
1000+
val rejectAdditionalFields = schema.annotations.exists(_.isInstanceOf[rejectExtraFields])
1001+
(trace: List[JsonError], in: RetractReader) => {
9861002
Lexer.char(trace, in, '{')
987-
if (Lexer.firstField(trace, in)) {
988-
while ({
989-
val field = Lexer.string(trace, in).toString
990-
structure.find(f => f.nameAndAliases.contains(field)) match {
991-
case Some(s @ Schema.Field(_, schema, _, _, _, _)) =>
992-
val fieldName = s.fieldName
993-
val trace_ = JsonError.ObjectAccess(fieldName) :: trace
994-
Lexer.char(trace_, in, ':')
995-
val value = schemaDecoder(schema).unsafeDecode(trace_, in)
996-
builder += ((JsonFieldDecoder.string.unsafeDecodeField(trace_, fieldName), value))
997-
case None if rejectAdditionalFields =>
998-
throw UnsafeJson(JsonError.Message(s"unexpected field: $field") :: trace)
999-
case None =>
1000-
Lexer.char(trace, in, ':')
1001-
Lexer.skipValue(trace, in)
1002-
1003-
}
1004-
Lexer.nextField(trace, in)
1005-
}) {
1006-
()
1007-
}
1008-
}
1009-
val tuples = builder.result()
1010-
val collectedFields: Set[String] = tuples.map { case (fieldName, _) => fieldName }.toSet
1011-
val resultBuilder = ListMap.newBuilder[String, Any]
1012-
1013-
// add fields with default values if they are not present in the JSON
1014-
structure.foreach { field =>
1015-
if (!collectedFields.contains(field.fieldName) && field.optional && field.defaultValue.isDefined) {
1016-
val value = field.fieldName -> field.defaultValue.get
1017-
resultBuilder += value
1003+
val map = defaults.clone().asInstanceOf[util.HashMap[String, Any]]
1004+
var continue = Lexer.firstField(trace, in)
1005+
while (continue) {
1006+
val fieldNameOrAlias = Lexer.string(trace, in).toString
1007+
val spanWithDecoder = spansWithDecoders.get(fieldNameOrAlias)
1008+
if (spanWithDecoder ne null) {
1009+
val span = spanWithDecoder._1
1010+
val dec = spanWithDecoder._2
1011+
val trace_ = span :: trace
1012+
Lexer.char(trace_, in, ':')
1013+
val fieldName = span.field
1014+
map.put(fieldName, dec.unsafeDecode(trace_, in))
1015+
} else if (rejectAdditionalFields) {
1016+
throw UnsafeJson(JsonError.Message(s"unexpected field: $fieldNameOrAlias") :: trace)
1017+
} else {
1018+
Lexer.char(trace, in, ':')
1019+
Lexer.skipValue(trace, in)
10181020
}
1021+
continue = Lexer.nextField(trace, in)
10191022
}
1020-
(resultBuilder ++= tuples).result()
1023+
(ListMap.newBuilder[String, Any] ++= ({ // to avoid O(n) insert operations
1024+
import scala.collection.JavaConverters.mapAsScalaMapConverter // use deprecated class for Scala 2.12 compatibility
1025+
1026+
map.asScala
1027+
}: @scala.annotation.nowarn)).result()
10211028
}
10221029
}
10231030

@@ -1116,16 +1123,20 @@ object JsonCodec {
11161123
val caseTpeNames = discriminatorTuple.map(_._2).toArray
11171124
(a: Z, indent: Option[Int], out: Write) => {
11181125
out.write('{')
1119-
val indent_ = bump(indent)
1120-
pad(indent_, out)
1126+
val doPrettyPrint = indent ne None
1127+
var indent_ = indent
1128+
if (doPrettyPrint) {
1129+
indent_ = bump(indent)
1130+
pad(indent_, out)
1131+
}
11211132
val strEnc = string.encoder
11221133
var first = true
11231134
var i = 0
11241135
while (i < tags.length) {
11251136
first = false
11261137
strEnc.unsafeEncode(tags(i), indent_, out)
1127-
if (indent.isEmpty) out.write(':')
1128-
else out.write(" : ")
1138+
if (doPrettyPrint) out.write(" : ")
1139+
else out.write(':')
11291140
strEnc.unsafeEncode(caseTpeNames(i), indent_, out)
11301141
i += 1
11311142
}
@@ -1139,15 +1150,15 @@ object JsonCodec {
11391150
if (first) first = false
11401151
else {
11411152
out.write(',')
1142-
if (indent.isDefined) pad(indent_, out)
1153+
if (doPrettyPrint) pad(indent_, out)
11431154
}
11441155
strEnc.unsafeEncode(schema.name, indent_, out)
1145-
if (indent.isEmpty) out.write(':')
1146-
else out.write(" : ")
1156+
if (doPrettyPrint) out.write(" : ")
1157+
else out.write(':')
11471158
enc.unsafeEncode(value, indent_, out)
11481159
}
11491160
}
1150-
pad(indent, out)
1161+
if (doPrettyPrint) pad(indent, out)
11511162
out.write('}')
11521163
}
11531164
}
@@ -1156,31 +1167,27 @@ object JsonCodec {
11561167
//scalafmt: { maxColumn = 400, optIn.configStyleArguments = false }
11571168
private[codec] object ProductDecoder {
11581169

1159-
private[codec] def caseClass0Decoder[Z](discriminator: Int, schema: Schema.CaseClass0[Z]): ZJsonDecoder[Z] = { (trace: List[JsonError], in: RetractReader) =>
1160-
def skipField(): Unit = {
1161-
val rejectExtraFields = schema.annotations.collectFirst({ case _: rejectExtraFields => () }).isDefined
1162-
if (rejectExtraFields) {
1163-
throw UnsafeJson(JsonError.Message("extra field") :: trace)
1164-
}
1165-
Lexer.char(trace, in, '"')
1166-
Lexer.skipString(trace, in)
1167-
Lexer.char(trace, in, ':')
1168-
Lexer.skipValue(trace, in)
1169-
}
1170-
1171-
if (discriminator == -2) {
1172-
while (Lexer.nextField(trace, in)) { skipField() }
1173-
} else {
1174-
if (discriminator == -1) {
1175-
Lexer.char(trace, in, '{')
1176-
}
1177-
if (Lexer.firstField(trace, in)) {
1178-
skipField()
1179-
while (Lexer.nextField(trace, in)) { skipField() }
1170+
private[codec] def caseClass0Decoder[Z](discriminator: Int, schema: Schema.CaseClass0[Z]): ZJsonDecoder[Z] = {
1171+
val rejectExtraFields = schema.annotations.exists(_.isInstanceOf[rejectExtraFields])
1172+
(trace: List[JsonError], in: RetractReader) => {
1173+
var continue =
1174+
if (discriminator == -2) Lexer.nextField(trace, in)
1175+
else {
1176+
if (discriminator == -1) Lexer.char(trace, in, '{')
1177+
Lexer.firstField(trace, in)
1178+
}
1179+
while (continue) {
1180+
if (rejectExtraFields) {
1181+
throw UnsafeJson(JsonError.Message("extra field") :: trace)
1182+
}
1183+
Lexer.char(trace, in, '"')
1184+
Lexer.skipString(trace, in)
1185+
Lexer.char(trace, in, ':')
1186+
Lexer.skipValue(trace, in)
1187+
continue = Lexer.nextField(trace, in)
11801188
}
1189+
schema.defaultConstruct()
11811190
}
1182-
1183-
schema.defaultConstruct()
11841191
}
11851192

11861193
private[codec] def caseClass1Decoder[A, Z](discriminator: Int, schema: Schema.CaseClass1[A, Z]): ZJsonDecoder[Z] = {
@@ -1571,13 +1578,15 @@ object JsonCodec {
15711578
if ((field.optional || field.transient) && field.defaultValue.isDefined) {
15721579
buffer(idx) = field.defaultValue.get
15731580
} else {
1574-
val schema = field.schema match {
1575-
case l @ Schema.Lazy(_) => l.schema
1576-
case s => s
1581+
var schema = field.schema
1582+
schema match {
1583+
case l: Schema.Lazy[_] => schema = l.schema
1584+
case _ =>
15771585
}
15781586
buffer(idx) = schema match {
1587+
case _: Schema.Optional[_] => None
15791588
case collection: Schema.Collection[_, _] => collection.empty
1580-
case _ => schemaDecoder(schema).unsafeDecodeMissing(spans(idx) :: trace)
1589+
case _ => error(spans(idx) :: trace, "missing")
15811590
}
15821591
}
15831592
}
@@ -1592,36 +1601,33 @@ object JsonCodec {
15921601

15931602
private object CaseClassJsonDecoder {
15941603

1595-
def apply[Z](caseClassSchema: Schema.Record[Z], discriminator: Int): CaseClassJsonDecoder[Z] = {
1596-
val len = caseClassSchema.fields.length
1604+
def apply[Z](schema: Schema.Record[Z], discriminator: Int): CaseClassJsonDecoder[Z] = {
1605+
val len = schema.fields.length
15971606
val fields = new Array[Schema.Field[Z, _]](len)
15981607
val decoders = new Array[ZJsonDecoder[_]](len)
15991608
val spans = new Array[JsonError.ObjectAccess](len)
16001609
val names = new Array[String](len)
1601-
var aliases = Map.empty[String, Int]
1610+
val aliases = Array.newBuilder[(String, Int)]
16021611
var i = 0
1603-
caseClassSchema.fields.foreach { field =>
1612+
schema.fields.foreach { field =>
16041613
val name = field.name.asInstanceOf[String]
16051614
fields(i) = field
16061615
names(i) = name
16071616
spans(i) = JsonError.ObjectAccess(name)
16081617
decoders(i) = schemaDecoder(field.schema)
16091618
field.annotations.foreach {
1610-
case annotation: fieldNameAliases =>
1611-
annotation.aliases.foreach { alias =>
1612-
aliases = aliases.updated(alias, i)
1613-
}
1614-
case _ =>
1619+
case annotation: fieldNameAliases => annotation.aliases.foreach(a => aliases += ((a, i)))
1620+
case _ =>
16151621
}
16161622
i += 1
16171623
}
16181624
new CaseClassJsonDecoder(
16191625
fields,
16201626
decoders,
16211627
spans,
1622-
new StringMatrix(names, aliases.toArray),
1628+
new StringMatrix(names, aliases.result()),
16231629
discriminator,
1624-
caseClassSchema.annotations.collectFirst({ case _: rejectExtraFields => () }).isDefined
1630+
schema.annotations.exists(_.isInstanceOf[rejectExtraFields])
16251631
)
16261632
}
16271633
}

0 commit comments

Comments
 (0)