@@ -23,7 +23,7 @@ import zio.json.{
2323import zio .prelude .NonEmptyMap
2424import zio .schema .Schema .GenericRecord
2525import zio .schema ._
26- import zio .schema .annotation .{ discriminatorName , rejectExtraFields , _ }
26+ import zio .schema .annotation .{ rejectExtraFields , _ }
2727import zio .schema .codec .DecodeError .ReadError
2828import zio .schema .codec .JsonCodec .JsonDecoder .schemaDecoder
2929import zio .stream .{ ZChannel , ZPipeline }
@@ -96,21 +96,134 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
9696 }
9797
9898 /**
99- * When disabled for decoding , matching fields will be omitted from the JSON. When disabled for encoding ,
99+ * When disabled for encoding , matching fields will be omitted from the JSON. When disabled for decoding ,
100100 * missing fields will be decoded as the default value.
101101 */
102102 case class ExplicitConfig (encoding : Boolean = true , decoding : Boolean = false )
103103
104104 final case class Configuration (
105105 explicitEmptyCollections : ExplicitConfig = ExplicitConfig (),
106+ explicitNulls : ExplicitConfig = ExplicitConfig (),
107+ discriminatorSettings : DiscriminatorSetting = DiscriminatorSetting .default,
108+ fieldNameFormat : NameFormat = NameFormat .Identity ,
106109 treatStreamsAsArrays : Boolean = false ,
107- explicitNulls : ExplicitConfig = ExplicitConfig ()
108- ) {}
110+ rejectExtraFields : Boolean = false
111+ ) {
112+
113+ val noDiscriminator : Boolean = discriminatorSettings match {
114+ case DiscriminatorSetting .NoDiscriminator => true
115+ case _ => false
116+ }
117+
118+ val discriminatorName : Option [String ] = discriminatorSettings match {
119+ case DiscriminatorSetting .Name (name, _) => Some (name)
120+ case _ => None
121+ }
122+
123+ val discriminatorFormat : NameFormat = discriminatorSettings match {
124+ case DiscriminatorSetting .ClassName (format) => format
125+ case DiscriminatorSetting .Name (_, format) => format
126+ case _ => NameFormat .Identity
127+ }
128+ }
109129
110130 object Configuration {
111131 val default : Configuration = Configuration ()
112132 }
113133
134+ sealed trait NameFormat extends (String => String )
135+
136+ object NameFormat {
137+ import java .lang .Character ._
138+
139+ private def enforceCamelOrPascalCase (s : String , toPascal : Boolean ): String =
140+ if (s.indexOf('_' ) == - 1 && s.indexOf('-' ) == - 1 ) {
141+ if (s.isEmpty) s
142+ else {
143+ val ch = s.charAt(0 )
144+ val fixedCh =
145+ if (toPascal) toUpperCase(ch)
146+ else toLowerCase(ch)
147+ s " $fixedCh${s.substring(1 )}"
148+ }
149+ } else {
150+ val len = s.length
151+ val sb = new StringBuilder (len)
152+ var i = 0
153+ var isPrecedingDash = toPascal
154+ while (i < len) isPrecedingDash = {
155+ val ch = s.charAt(i)
156+ i += 1
157+ (ch == '_' || ch == '-' ) || {
158+ val fixedCh =
159+ if (isPrecedingDash) toUpperCase(ch)
160+ else toLowerCase(ch)
161+ sb.append(fixedCh)
162+ false
163+ }
164+ }
165+ sb.toString
166+ }
167+
168+ private def enforceSnakeOrKebabCase (s : String , separator : Char ): String = {
169+ val len = s.length
170+ val sb = new StringBuilder (len << 1 )
171+ var i = 0
172+ var isPrecedingNotUpperCased = false
173+ while (i < len) isPrecedingNotUpperCased = {
174+ val ch = s.charAt(i)
175+ i += 1
176+ if (ch == '_' || ch == '-' ) {
177+ sb.append(separator)
178+ false
179+ } else if (! isUpperCase(ch)) {
180+ sb.append(ch)
181+ true
182+ } else {
183+ if (isPrecedingNotUpperCased || i > 1 && i < len && ! isUpperCase(s.charAt(i))) sb.append(separator)
184+ sb.append(toLowerCase(ch))
185+ false
186+ }
187+ }
188+ sb.toString
189+ }
190+
191+ case class Custom (f : String => String ) extends NameFormat {
192+ override def apply (memberName : String ): String = f(memberName)
193+ }
194+
195+ case object SnakeCase extends NameFormat {
196+ override def apply (memberName : String ): String = enforceSnakeOrKebabCase(memberName, '_' )
197+ }
198+
199+ case object CamelCase extends NameFormat {
200+ override def apply (memberName : String ): String =
201+ enforceCamelOrPascalCase(memberName, toPascal = false )
202+ }
203+
204+ case object PascalCase extends NameFormat {
205+ override def apply (memberName : String ): String =
206+ enforceCamelOrPascalCase(memberName, toPascal = true )
207+ }
208+
209+ case object KebabCase extends NameFormat {
210+ override def apply (memberName : String ): String = enforceSnakeOrKebabCase(memberName, '-' )
211+ }
212+
213+ case object Identity extends NameFormat {
214+ override def apply (memberName : String ): String = memberName
215+ }
216+ }
217+
218+ sealed trait DiscriminatorSetting
219+
220+ object DiscriminatorSetting {
221+ val default : ClassName = ClassName (NameFormat .Identity )
222+ case class ClassName (format : NameFormat ) extends DiscriminatorSetting
223+ case object NoDiscriminator extends DiscriminatorSetting
224+ case class Name (name : String , format : NameFormat = NameFormat .Identity ) extends DiscriminatorSetting
225+ }
226+
114227 type DiscriminatorTuple = Option [(String , String )]
115228
116229 implicit def zioJsonBinaryCodec [A ](implicit jsonCodec : ZJsonCodec [A ]): BinaryCodec [A ] =
@@ -579,32 +692,38 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
579692 }
580693 }
581694
582- private def enumEncoder [Z ](schema : Schema .Enum [Z ], cfg : Configuration ): ZJsonEncoder [Z ] =
695+ private def enumEncoder [Z ](schema : Schema .Enum [Z ], cfg : Configuration ): ZJsonEncoder [Z ] = {
696+ def format (caseName : String ): String =
697+ if (cfg.discriminatorFormat == NameFormat .Identity ) caseName
698+ else cfg.discriminatorFormat(caseName)
583699 // if all cases are CaseClass0, encode as a String
584700 if (schema.annotations.exists(_.isInstanceOf [simpleEnum])) {
585701 val caseMap : Map [Z , String ] =
586702 schema.nonTransientCases
587703 .map(
588704 case_ =>
589705 case_.schema.asInstanceOf [Schema .CaseClass0 [Z ]].defaultConstruct() ->
590- case_.caseName
706+ format( case_.caseName)
591707 )
592708 .toMap
593709 ZJsonEncoder .string.contramap(caseMap(_))
594710 } else {
595711 val discriminatorName =
596- if (schema.noDiscriminator) None
597- else schema.annotations.collectFirst { case d : discriminatorName => d.tag }
598- val doJsonObjectWrapping = discriminatorName.isEmpty && ! schema.noDiscriminator
712+ if (schema.noDiscriminator || (cfg.noDiscriminator && schema.discriminatorName.isEmpty)) None
713+ else schema.discriminatorName.orElse(cfg.discriminatorName)
714+ val doJsonObjectWrapping =
715+ discriminatorName.isEmpty &&
716+ ! schema.noDiscriminator &&
717+ ! cfg.noDiscriminator
599718 if (doJsonObjectWrapping) {
600719 new ZJsonEncoder [Z ] {
601720 private [this ] val cases = schema.nonTransientCases.toArray
602721 private [this ] val decoders =
603722 cases.map(case_ => schemaEncoder(case_.schema.asInstanceOf [Schema [Any ]], cfg, None ))
604723 private [this ] val encodedKeys =
605- cases.map(case_ => ZJsonEncoder .string.encodeJson(case_.caseName).toString + ':' )
724+ cases.map(case_ => ZJsonEncoder .string.encodeJson(format( case_.caseName) ).toString + ':' )
606725 private [this ] val prettyEncodedKeys =
607- cases.map(case_ => ZJsonEncoder .string.encodeJson(case_.caseName).toString + " : " )
726+ cases.map(case_ => ZJsonEncoder .string.encodeJson(format( case_.caseName) ).toString + " : " )
608727
609728 override def unsafeEncode (a : Z , indent : Option [Int ], out : Write ): Unit = {
610729 var idx = 0
@@ -634,7 +753,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
634753 if (discriminatorName eq None ) None
635754 else {
636755 val key = ZJsonEncoder .string.encodeJson(discriminatorName.get, None ).toString
637- val value = ZJsonEncoder .string.encodeJson(case_.caseName, None ).toString
756+ val value = ZJsonEncoder .string.encodeJson(format( case_.caseName) , None ).toString
638757 Some ((key + ':' + value, key + " : " + value))
639758 }
640759 schemaEncoder(case_.schema.asInstanceOf [Schema [Any ]], cfg, discriminatorTuple)
@@ -655,6 +774,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
655774 }
656775 }
657776 }
777+ }
658778
659779 private def fallbackEncoder [A , B ](left : ZJsonEncoder [A ], right : ZJsonEncoder [B ]): ZJsonEncoder [Fallback [A , B ]] =
660780 new ZJsonEncoder [Fallback [A , B ]] {
@@ -686,10 +806,14 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
686806 out.write(" {}" )
687807 } else {
688808 val encoders = nonTransientFields.map(field => schemaEncoder(field.schema.asInstanceOf [Schema [Any ]], cfg))
809+ def name (field : Schema .Field [_, _]): String =
810+ if (cfg.fieldNameFormat == JsonCodec .NameFormat .Identity ) field.fieldName
811+ else if (field.fieldName == field.name) cfg.fieldNameFormat(field.fieldName)
812+ else field.fieldName
689813 val encodedNames =
690- nonTransientFields.map(field => ZJsonEncoder .string.encodeJson(field.fieldName , None ).toString + ':' )
814+ nonTransientFields.map(field => ZJsonEncoder .string.encodeJson(name( field) , None ).toString + ':' )
691815 val prettyEncodedNames =
692- nonTransientFields.map(field => ZJsonEncoder .string.encodeJson(field.fieldName , None ).toString + " : " )
816+ nonTransientFields.map(field => ZJsonEncoder .string.encodeJson(name( field) , None ).toString + " : " )
693817 (a : ListMap [String , _], indent : Option [Int ], out : Write ) => {
694818 out.write('{' )
695819 val indent_ = bump(indent)
@@ -801,7 +925,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
801925 case s : Schema .GenericRecord => recordDecoder(s, discriminator, config)
802926 case s : Schema .Enum [A ] => enumDecoder(s, config)
803927 // case Schema.Meta(_, _) => astDecoder
804- case s @ Schema .CaseClass0 (_, _, _) => caseClass0Decoder(discriminator, s)
928+ case s @ Schema .CaseClass0 (_, _, _) => caseClass0Decoder(discriminator, s, config )
805929 case s @ Schema .CaseClass1 (_, _, _, _) => caseClass1Decoder(discriminator, s, config)
806930 case s @ Schema .CaseClass2 (_, _, _, _, _) => caseClass2Decoder(discriminator, s, config)
807931 case s @ Schema .CaseClass3 (_, _, _, _, _, _) => caseClass3Decoder(discriminator, s, config)
@@ -889,10 +1013,13 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
8891013 }
8901014
8911015 private def enumDecoder [Z ](parentSchema : Schema .Enum [Z ], config : Configuration ): ZJsonDecoder [Z ] = {
1016+ def format (caseName : String ): String =
1017+ if (config.discriminatorFormat == NameFormat .Identity ) caseName
1018+ else config.discriminatorFormat(caseName)
8921019 val caseNameAliases = new mutable.HashMap [String , Schema .Case [Z , Any ]]
8931020 parentSchema.cases.foreach { case_ =>
8941021 val schema = case_.asInstanceOf [Schema .Case [Z , Any ]]
895- caseNameAliases.put(case_.caseName, schema)
1022+ caseNameAliases.put(format( case_.caseName) , schema)
8961023 case_.caseNameAliases.foreach(a => caseNameAliases.put(a, schema))
8971024 }
8981025
@@ -926,7 +1053,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
9261053 }
9271054 }
9281055 }
929- } else if (parentSchema.annotations.exists(_. isInstanceOf [ noDiscriminator]) ) {
1056+ } else if (parentSchema.noDiscriminator || config. noDiscriminator) {
9301057 new ZJsonDecoder [Z ] {
9311058 private [this ] val decoders = parentSchema.cases.map(c => schemaDecoder(c.schema, config))
9321059
@@ -946,13 +1073,13 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
9461073 }
9471074 }
9481075 } else {
949- val discriminator = parentSchema.annotations.collectFirst { case d : discriminatorName => d.tag }
1076+ val discriminator = parentSchema.discriminatorName.orElse(config.discriminatorName)
9501077 discriminator match {
9511078 case None =>
9521079 if (caseNameAliases.size <= 64 ) {
9531080 val caseMatrix = new StringMatrix (caseNameAliases.keys.toArray)
9541081 val cases = caseNameAliases.values.map { case_ =>
955- (JsonError .ObjectAccess (case_.caseName), schemaDecoder(case_.schema, config))
1082+ (JsonError .ObjectAccess (format( case_.caseName) ), schemaDecoder(case_.schema, config))
9561083 }.toArray
9571084 (trace : List [JsonError ], in : RetractReader ) => {
9581085 val lexer = Lexer
@@ -971,7 +1098,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
9711098 new util.HashMap [String , (JsonError .ObjectAccess , ZJsonDecoder [Any ])](caseNameAliases.size << 1 )
9721099 caseNameAliases.foreach {
9731100 case (name, case_) =>
974- cases.put(name, (JsonError .ObjectAccess (case_.caseName), schemaDecoder(case_.schema, config)))
1101+ cases.put(name, (JsonError .ObjectAccess (format( case_.caseName) ), schemaDecoder(case_.schema, config)))
9751102 }
9761103 (trace : List [JsonError ], in : RetractReader ) => {
9771104 val lexer = Lexer
@@ -993,7 +1120,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
9931120 if (caseNameAliases.size <= 64 ) {
9941121 val caseMatrix = new StringMatrix (caseNameAliases.keys.toArray)
9951122 val cases = caseNameAliases.values.map { case_ =>
996- (JsonError .ObjectAccess (case_.caseName), schemaDecoder(case_.schema, config, discriminator))
1123+ (JsonError .ObjectAccess (format( case_.caseName) ), schemaDecoder(case_.schema, config, discriminator))
9971124 }.toArray
9981125 (trace : List [JsonError ], in : RetractReader ) => {
9991126 val lexer = Lexer
@@ -1020,7 +1147,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
10201147 case (name, case_) =>
10211148 cases.put(
10221149 name,
1023- (JsonError .ObjectAccess (case_.caseName), schemaDecoder(case_.schema, config, discriminator))
1150+ (JsonError .ObjectAccess (format( case_.caseName) ), schemaDecoder(case_.schema, config, discriminator))
10241151 )
10251152 }
10261153 (trace : List [JsonError ], in : RetractReader ) => {
@@ -1223,10 +1350,14 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
12231350 private [codec] def caseClassEncoder [Z ](schema : Schema .Record [Z ], cfg : Configuration , discriminatorTuple : DiscriminatorTuple ): ZJsonEncoder [Z ] = {
12241351 val nonTransientFields = schema.nonTransientFields.toArray.asInstanceOf [Array [Schema .Field [Z , Any ]]]
12251352 val encoders = nonTransientFields.map(s => JsonEncoder .schemaEncoder(s.schema, cfg))
1353+ def name (field : Schema .Field [_, _]): String =
1354+ if (cfg.fieldNameFormat == JsonCodec .NameFormat .Identity ) field.fieldName
1355+ else if (field.fieldName == field.name) cfg.fieldNameFormat(field.fieldName)
1356+ else field.fieldName
12261357 val encodedNames =
1227- nonTransientFields.map(field => ZJsonEncoder .string.encodeJson(field.fieldName , None ).toString + ':' )
1358+ nonTransientFields.map(field => ZJsonEncoder .string.encodeJson(name( field) , None ).toString + ':' )
12281359 val prettyEncodedNames =
1229- nonTransientFields.map(field => ZJsonEncoder .string.encodeJson(field.fieldName , None ).toString + " : " )
1360+ nonTransientFields.map(field => ZJsonEncoder .string.encodeJson(name( field) , None ).toString + " : " )
12301361 (a : Z , indent : Option [Int ], out : Write ) => {
12311362 out.write('{' )
12321363 val indent_ = bump(indent)
@@ -1260,8 +1391,8 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
12601391 // scalafmt: { maxColumn = 400, optIn.configStyleArguments = false }
12611392 private [codec] object ProductDecoder {
12621393
1263- private [codec] def caseClass0Decoder [Z ](discriminator : Option [String ], schema : Schema .CaseClass0 [Z ]): ZJsonDecoder [Z ] = {
1264- val rejectExtraFields = schema.annotations.exists(_. isInstanceOf [ rejectExtraFields])
1394+ private [codec] def caseClass0Decoder [Z ](discriminator : Option [String ], schema : Schema .CaseClass0 [Z ], config : Configuration ): ZJsonDecoder [Z ] = {
1395+ val rejectExtraFields = schema.rejectExtraFields || config. rejectExtraFields
12651396 val noDiscriminator = discriminator.isEmpty
12661397 (trace : List [JsonError ], in : RetractReader ) => {
12671398 val lexer = Lexer
@@ -1740,10 +1871,13 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
17401871 schema.fields.foreach { field =>
17411872 fields(idx) = field
17421873 decoders(idx) = schemaDecoder(field.schema, config)
1743- val name = field.fieldName
1874+ val name =
1875+ if (config.fieldNameFormat == JsonCodec .NameFormat .Identity ) field.fieldName
1876+ else if (field.fieldName == field.name) config.fieldNameFormat(field.fieldName)
1877+ else field.fieldName
17441878 names(idx) = name
17451879 spans(idx) = JsonError .ObjectAccess (name)
1746- (field.nameAndAliases - name ).foreach { a =>
1880+ (field.nameAndAliases - field.fieldName ).foreach { a =>
17471881 aliases(aliasIdx) = (a, idx)
17481882 aliasIdx += 1
17491883 }
@@ -1760,7 +1894,7 @@ JsonCodec.Configuration makes it now possible to configure en-/decoding of empty
17601894 spans,
17611895 new StringMatrix (names, aliases),
17621896 ! hasDiscriminator,
1763- ! schema.annotations.exists(_. isInstanceOf [ rejectExtraFields]) ,
1897+ ! schema.rejectExtraFields && ! config. rejectExtraFields,
17641898 config
17651899 )
17661900 }
0 commit comments