Skip to content

Commit 00110cc

Browse files
authored
Fix #758 by proper decoding of backticked names of fields and classes (#769)
1 parent 2902d21 commit 00110cc

File tree

3 files changed

+73
-40
lines changed

3 files changed

+73
-40
lines changed

zio-schema-derivation/shared/src/main/scala-2/zio/schema/DeriveSchema.scala

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package zio.schema
22

3+
import scala.reflect.NameTransformer
34
import scala.reflect.macros.whitebox
45

56
import zio.Chunk
@@ -40,6 +41,11 @@ object DeriveSchema {
4041

4142
def isMap(tpe: Type): Boolean = tpe.typeSymbol.fullName == "scala.collection.immutable.Map"
4243

44+
def decodeName(s: Symbol): String = NameTransformer.decode(s.name.toString)
45+
46+
def decodeFieldName(s: Symbol): String =
47+
NameTransformer.decode(s.name.toString.trim) // why is there a space at the end of field name?!
48+
4349
def collectTypeAnnotations(tpe: Type): List[Tree] =
4450
tpe.typeSymbol.annotations.collect {
4551
case annotation if !(annotation.tree.tpe <:< JavaAnnotationTpe) =>
@@ -235,7 +241,7 @@ object DeriveSchema {
235241
val genericAnnotations: List[Tree] =
236242
if (tpe.typeArgs.isEmpty) Nil
237243
else {
238-
val typeMembers = tpe.typeSymbol.asClass.typeParams.map(_.name.toString)
244+
val typeMembers = tpe.typeSymbol.asClass.typeParams.map(decodeName)
239245
val typeArgs = tpe.typeArgs
240246
.map(_.typeSymbol.fullName)
241247
.map(t => q"_root_.zio.schema.TypeId.parse(${t}).asInstanceOf[_root_.zio.schema.TypeId.Nominal]")
@@ -334,7 +340,7 @@ object DeriveSchema {
334340
concreteType(tpe, termSymbol.typeSignature),
335341
currentFrame +: stack
336342
)
337-
val fieldLabel = termSymbol.name.toString.trim
343+
val fieldLabel = decodeFieldName(termSymbol)
338344
val getFunc =
339345
q" (z: _root_.scala.collection.immutable.ListMap[String, _]) => z.apply($fieldLabel).asInstanceOf[${termSymbol.typeSignature}]"
340346

@@ -353,12 +359,13 @@ object DeriveSchema {
353359
val fromMap = {
354360
val casts = fieldTypes.zip(fieldAnnotations).map {
355361
case (termSymbol, annotations) =>
356-
val newName = getFieldName(annotations).getOrElse(termSymbol.name.toString.trim)
362+
val fieldLabel = decodeFieldName(termSymbol)
363+
val newName = getFieldName(annotations).getOrElse(fieldLabel)
357364
q"""
358365
try m.apply(${newName}).asInstanceOf[${termSymbol.typeSignature}]
359366
catch {
360-
case _: ClassCastException => throw new RuntimeException("Field " + ${termSymbol.name.toString.trim} + " has invalid type")
361-
case _: Throwable => throw new RuntimeException("Field " + ${termSymbol.name.toString.trim} + " is missing")
367+
case _: ClassCastException => throw new RuntimeException("Field " + $fieldLabel + " has invalid type")
368+
case _: Throwable => throw new RuntimeException("Field " + $fieldLabel + " is missing")
362369
}
363370
"""
364371
}
@@ -414,8 +421,8 @@ object DeriveSchema {
414421
currentFrame +: stack
415422
)
416423
val fieldArg = if (fieldTypes.size > 1) TermName(s"field0${idx + 1}") else TermName("field0")
417-
val fieldLabel = termSymbol.name.toString.trim
418-
val getArg = TermName(fieldLabel)
424+
val fieldLabel = decodeFieldName(termSymbol)
425+
val getArg = TermName(termSymbol.name.toString.trim)
419426

420427
val getFunc = q" (z: $tpe) => z.$getArg"
421428
val setFunc = q" (z: $tpe, v: $fieldType) => z.copy[..${tpe.typeArgs}]($getArg = v)"
@@ -459,9 +466,9 @@ object DeriveSchema {
459466

460467
val typeArgsWithFields = fieldTypes.zip(fieldAnnotations).map {
461468
case (termSymbol, annotations) if annotations.nonEmpty =>
462-
tq"${getFieldName(annotations).getOrElse(termSymbol.name.toString.trim)}.type"
469+
tq"${getFieldName(annotations).getOrElse(decodeFieldName(termSymbol))}.type"
463470
case (termSymbol, _) =>
464-
tq"${termSymbol.name.toString.trim}.type"
471+
tq"${decodeFieldName(termSymbol)}.type"
465472
} ++ typeArgs
466473

467474
fieldTypes.size match {
@@ -524,12 +531,12 @@ object DeriveSchema {
524531
val typeId = q"_root_.zio.schema.TypeId.parse(${tpe.toString()})"
525532

526533
val appliedTypeArgs: Map[String, Type] =
527-
tpe.typeConstructor.typeParams.map(_.name.toString).zip(tpe.typeArgs).toMap
534+
tpe.typeConstructor.typeParams.map(decodeName).zip(tpe.typeArgs).toMap
528535

529536
def appliedSubtype(subtype: Type): Type =
530537
if (subtype.typeArgs.size == 0) subtype
531538
else {
532-
val appliedTypes = subtype.typeConstructor.typeParams.map(_.name.toString).map { typeParam =>
539+
val appliedTypes = subtype.typeConstructor.typeParams.map(decodeName).map { typeParam =>
533540
appliedTypeArgs.get(typeParam) match {
534541
case None =>
535542
c.abort(
@@ -583,7 +590,7 @@ object DeriveSchema {
583590
val genericAnnotations: List[Tree] =
584591
if (tpe.typeArgs.isEmpty) Nil
585592
else {
586-
val typeMembers = tpe.typeSymbol.asClass.typeParams.map(_.name.toString)
593+
val typeMembers = tpe.typeSymbol.asClass.typeParams.map(decodeName)
587594
val typeArgs = tpe.typeArgs
588595
.map(_.typeSymbol.fullName)
589596
.map(t => q"_root_.zio.schema.TypeId.parse(${t}).asInstanceOf[_root_.zio.schema.TypeId.Nominal]")
@@ -641,7 +648,7 @@ object DeriveSchema {
641648
val genericAnnotations: List[Tree] =
642649
if (subtype.typeArgs.isEmpty) Nil
643650
else {
644-
val typeMembers = subtype.typeSymbol.asClass.typeParams.map(_.name.toString)
651+
val typeMembers = subtype.typeSymbol.asClass.typeParams.map(decodeName)
645652
val typeArgs = subtype.typeArgs
646653
.map(_.typeSymbol.fullName)
647654
.map(t => q"_root_.zio.schema.TypeId.parse(${t}).asInstanceOf[_root_.zio.schema.TypeId.Nominal]")
@@ -668,7 +675,7 @@ object DeriveSchema {
668675
EmptyTree
669676
}.filter(_ != EmptyTree) ++ genericAnnotations
670677

671-
val caseLabel = subtype.typeSymbol.name.toString.trim
678+
val caseLabel = decodeName(subtype.typeSymbol)
672679
val caseSchema = directInferSchema(tpe, concreteType(tpe, subtype), currentFrame +: stack)
673680
val deconstructFn = q"(z: $tpe) => z.asInstanceOf[$subtype]"
674681
val constructFn = q"(z: $subtype) => z.asInstanceOf[$tpe]"

zio-schema-derivation/shared/src/test/scala/zio/schema/DeriveSchemaSpec.scala

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ object DeriveSchemaSpec extends ZIOSpecDefault with VersionSpecificDeriveSchemaS
111111
f21: Int = 21,
112112
f22: Int = 22,
113113
f23: Int = 23,
114-
f24: Int = 24
114+
`f-24`: Int = 24
115115
)
116116

117117
object Arity24 {
@@ -140,7 +140,7 @@ object DeriveSchemaSpec extends ZIOSpecDefault with VersionSpecificDeriveSchemaS
140140
arity19: (User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User),
141141
arity20: (User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User),
142142
arity21: (User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User),
143-
arity22: (User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User)
143+
`arity-22`: (User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User, User)
144144
)
145145
//scalafmt: { maxColumn = 120, optIn.configStyleArguments = true }
146146

@@ -258,11 +258,11 @@ object DeriveSchemaSpec extends ZIOSpecDefault with VersionSpecificDeriveSchemaS
258258
case class SimpleClass1() extends SimpleEnum1
259259

260260
sealed trait SimpleEnum2
261-
case class SimpleClass2() extends SimpleEnum2
261+
case class `Simple-Class-2`() extends SimpleEnum2
262262

263263
sealed abstract class AbstractBaseClass(val x: Int)
264-
final case class ConcreteClass1(override val x: Int, y: Int) extends AbstractBaseClass(x)
265-
final case class ConcreteClass2(override val x: Int, s: String) extends AbstractBaseClass(x)
264+
final case class ConcreteClass1(override val x: Int, y: Int) extends AbstractBaseClass(x)
265+
final case class `Concrete-Class-2`(override val x: Int, s: String) extends AbstractBaseClass(x)
266266

267267
sealed abstract class AbstractBaseClass2(val x: Int)
268268
sealed abstract class MiddleClass(override val x: Int, val y: Int) extends AbstractBaseClass2(x)
@@ -288,13 +288,17 @@ object DeriveSchemaSpec extends ZIOSpecDefault with VersionSpecificDeriveSchemaS
288288
assert(Schema[User].toString)(not(containsString("null")) && not(equalTo("$Lazy$")))
289289
},
290290
test("correctly derives case class with arity > 22") {
291-
assert(Schema[Arity24].toString)(not(containsString("null")) && not(equalTo("$Lazy$")))
291+
assert(Schema[Arity24].toString)(
292+
not(containsString("null")) && not(equalTo("$Lazy$") && containsString("f-24"))
293+
)
292294
},
293295
test("correctly derives recursive data structure") {
294296
assert(Schema[Recursive].toString)(not(containsString("null")) && not(equalTo("$Lazy$")))
295297
},
296298
test("correctly derives tuple arities from 2 to 22") {
297-
assert(Schema[TupleArities].toString)(not(containsString("null")) && not(equalTo("$Lazy$")))
299+
assert(Schema[TupleArities].toString)(
300+
not(containsString("null")) && not(equalTo("$Lazy$") && containsString("arity-22"))
301+
)
298302
},
299303
test("correctly derive mutually recursive data structure") {
300304
val c = Cyclic(1, CyclicChild1(2, CyclicChild2("3", None)))
@@ -526,26 +530,26 @@ object DeriveSchemaSpec extends ZIOSpecDefault with VersionSpecificDeriveSchemaS
526530
(a: AbstractBaseClass) => a.isInstanceOf[ConcreteClass1]
527531
),
528532
Schema.Case(
529-
"ConcreteClass2",
533+
"Concrete-Class-2",
530534
Schema.CaseClass2(
531-
TypeId.parse("zio.schema.DeriveSchemaSpec.ConcreteClass2"),
532-
field01 = Schema.Field[ConcreteClass2, Int](
535+
TypeId.parse("zio.schema.DeriveSchemaSpec.Concrete-Class-2"),
536+
field01 = Schema.Field[`Concrete-Class-2`, Int](
533537
"x",
534538
Schema.Primitive(StandardType.IntType),
535539
get0 = _.x,
536540
set0 = (a, b: Int) => a.copy(x = b)
537541
),
538-
field02 = Schema.Field[ConcreteClass2, String](
542+
field02 = Schema.Field[`Concrete-Class-2`, String](
539543
"s",
540544
Schema.Primitive(StandardType.StringType),
541545
get0 = _.s,
542546
set0 = (a, b: String) => a.copy(s = b)
543547
),
544-
ConcreteClass2.apply
548+
`Concrete-Class-2`.apply
545549
),
546-
(a: AbstractBaseClass) => a.asInstanceOf[ConcreteClass2],
547-
(a: ConcreteClass2) => a.asInstanceOf[AbstractBaseClass],
548-
(a: AbstractBaseClass) => a.isInstanceOf[ConcreteClass2]
550+
(a: AbstractBaseClass) => a.asInstanceOf[`Concrete-Class-2`],
551+
(a: `Concrete-Class-2`) => a.asInstanceOf[AbstractBaseClass],
552+
(a: AbstractBaseClass) => a.isInstanceOf[`Concrete-Class-2`]
549553
),
550554
Chunk.empty
551555
)

zio-schema-json/shared/src/test/scala/zio/schema/codec/JsonCodecSpec.scala

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ object JsonCodecSpec extends ZIOSpecDefault {
9292
)
9393
}
9494
),
95+
suite("case class")(
96+
test("backticked field name") {
97+
assertEncodesJson(
98+
Schema[BacktickedFieldName],
99+
BacktickedFieldName("test"),
100+
"""{"x-api-key":"test"}"""
101+
)
102+
}
103+
),
95104
suite("optional field annotation")(
96105
test("list empty") {
97106
assertEncodesJson(
@@ -868,6 +877,13 @@ object JsonCodecSpec extends ZIOSpecDefault {
868877
charSequenceToByteChunk("""{"query":"test","pageNumber":0,"resultPerPage":10}""")
869878
)
870879
},
880+
test("backticked field name") {
881+
assertDecodes(
882+
BacktickedFieldName.schema,
883+
BacktickedFieldName("test"),
884+
charSequenceToByteChunk("""{"x-api-key":"test"}""")
885+
)
886+
},
871887
test("field name with alias - id") {
872888
assertDecodes(
873889
Order.schema,
@@ -1599,7 +1615,7 @@ object JsonCodecSpec extends ZIOSpecDefault {
15991615
Enumeration2(StringValue2("foo"))
16001616
) &> assertEncodesThenDecodes(
16011617
Schema[Enumeration2],
1602-
Enumeration2(StringValue2Multi("foo", "bar"))
1618+
Enumeration2(`StringValue2-Backticked`("foo", "bar"))
16031619
) &> assertEncodesThenDecodes(Schema[Enumeration2], Enumeration2(IntValue2(-1))) &> assertEncodesThenDecodes(
16041620
Schema[Enumeration2],
16051621
Enumeration2(BooleanValue2(false))
@@ -1611,7 +1627,7 @@ object JsonCodecSpec extends ZIOSpecDefault {
16111627
Enumeration3(StringValue3("foo"))
16121628
) &> assertEncodesThenDecodes(
16131629
Schema[Enumeration3],
1614-
Enumeration3(StringValue3Multi("foo", "bar"))
1630+
Enumeration3(`StringValue3-Backticked`("foo", "bar"))
16151631
)
16161632
},
16171633
test("of case classes with discriminator") {
@@ -2147,10 +2163,10 @@ object JsonCodecSpec extends ZIOSpecDefault {
21472163

21482164
@discriminatorName("_type")
21492165
sealed trait OneOf2
2150-
case class StringValue2(value: String) extends OneOf2
2151-
case class IntValue2(value: Int) extends OneOf2
2152-
case class BooleanValue2(value: Boolean) extends OneOf2
2153-
case class StringValue2Multi(value1: String, value2: String) extends OneOf2
2166+
case class StringValue2(value: String) extends OneOf2
2167+
case class IntValue2(value: Int) extends OneOf2
2168+
case class BooleanValue2(value: Boolean) extends OneOf2
2169+
case class `StringValue2-Backticked`(value1: String, value2: String) extends OneOf2
21542170

21552171
case class Enumeration2(oneOf: OneOf2)
21562172

@@ -2160,11 +2176,11 @@ object JsonCodecSpec extends ZIOSpecDefault {
21602176

21612177
@noDiscriminator
21622178
sealed trait OneOf3
2163-
case class StringValue3(value: String) extends OneOf3
2164-
case class IntValue3(value: Int) extends OneOf3
2165-
case class BooleanValue3(value: Boolean) extends OneOf3
2166-
case class StringValue3Multi(value1: String, value2: String) extends OneOf3
2167-
case class Nested(oneOf: OneOf3) extends OneOf3
2179+
case class StringValue3(value: String) extends OneOf3
2180+
case class IntValue3(value: Int) extends OneOf3
2181+
case class BooleanValue3(value: Boolean) extends OneOf3
2182+
case class `StringValue3-Backticked`(value1: String, value2: String) extends OneOf3
2183+
case class Nested(oneOf: OneOf3) extends OneOf3
21682184

21692185
case class Enumeration3(oneOf: OneOf3)
21702186

@@ -2449,4 +2465,10 @@ object JsonCodecSpec extends ZIOSpecDefault {
24492465
object Recursive {
24502466
implicit val schema: Schema[Recursive] = DeriveSchema.gen
24512467
}
2468+
2469+
case class BacktickedFieldName(`x-api-key`: String)
2470+
2471+
object BacktickedFieldName {
2472+
implicit val schema: Schema[BacktickedFieldName] = DeriveSchema.gen
2473+
}
24522474
}

0 commit comments

Comments
 (0)