All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker.scala Maven / Gradle / Ivy

package com.github.plokhotnyuk.jsoniter_scala.macros

import java.lang.Character._
import java.time._
import java.math.MathContext
import java.util.concurrent.ConcurrentHashMap
import com.github.plokhotnyuk.jsoniter_scala.core._
import com.github.plokhotnyuk.jsoniter_scala.macros.CompileTimeEval._
import scala.language.implicitConversions
import scala.annotation._
import scala.annotation.meta.field
import scala.collection.{BitSet, immutable, mutable}
import scala.collection.mutable.Growable
import scala.collection.mutable.ArrayBuffer
import scala.util.control.NonFatal
import scala.quoted._
import scala.reflect.ClassTag

@field
final class named(val name: String) extends StaticAnnotation

@field
final class transient extends StaticAnnotation

@field
final class stringified extends StaticAnnotation

/**
  * Configuration parameter for `JsonCodecMaker.make()` call.
  *
  * BEWARE: a parameter of the `make` macro should not depend on code from the same compilation module where it is called.
  * Use a separated submodule of the project to compile all such dependencies before their usage for generation of codecs.
  *
  * Examples of `fieldNameMapper`, `javaEnumValueNameMapper`, and `adtLeafClassNameMapper` functions that have no
  * dependencies in the same compilation module are: `JsonCodecMaker.enforceCamelCase`, `JsonCodecMaker.enforce_snake_case`,
  * `JsonCodecMaker.enforce-kebab-case`, and `JsonCodecMaker.simpleClassName`. Or their composition like:
  * `s => JsonCodecMaker.enforce_snake_case(JsonCodecMaker.simpleClassName(s))`
  *
  * @param fieldNameMapper        the partial function of mapping from string of case class field name to JSON key
  *                               (an identity function by default)
  * @param javaEnumValueNameMapper the partial function of mapping from string of Java enum name to JSON key
  *                               (an identity function by default)
  * @param adtLeafClassNameMapper the function of mapping from string of case class/object full name to string value of
  *                               discriminator field (a function that truncate to simple class name by default)
  * @param discriminatorFieldName an optional name of discriminator field, where None can be used for alternative
  *                               representation of ADTs without the discriminator field (Some("type") by default)
  * @param isStringified          a flag that turns on stringification of number or boolean values of collections,
  *                               options and value classes (turned off by default)
  * @param mapAsArray             a flag that turns on serialization and parsing of maps as a JSON array (or sequences
  *                               of tuples) instead of a JSON object, that allow to use 'JsonValueCodec' for encoding
  *                               and decoding of keys (turned off by default)
  * @param skipUnexpectedFields   a flag that turns on skipping of unexpected fields or in other case a parse exception
  *                               will be thrown (turned on by default)
  * @param transientDefault       (always OFF in scala3)  a flag that turns on skipping serialization of fields that have same values as
  *                               default values defined for them in the primary constructor (turned on by default)
  * @param transientEmpty         a flag that turns on skipping serialization of fields that have empty values of
  *                               arrays or collections (turned on by default)
  * @param transientNone          a flag that turns on skipping serialization of fields that have empty values of
  *                               options (turned on by default)
  * @param bigDecimalPrecision    a precision in 'BigDecimal' values (34 by default)
  * @param bigDecimalScaleLimit   an exclusive limit for accepted scale in 'BigDecimal' values (6178 by default)
  * @param bigDecimalDigitsLimit  an exclusive limit for accepted number of mantissa digits of to be parsed before
  *                               rounding with the precision specified for 'BigDecimal' values (308 by default)
  * @param bigIntDigitsLimit      an exclusive limit for accepted number of decimal digits in 'BigInt' values
  *                               (308 by default)
  * @param bitSetValueLimit       an exclusive limit for accepted numeric values in bit sets (1024 by default)
  * @param mapMaxInsertNumber     a max number of inserts into maps (1024 by default)
  * @param setMaxInsertNumber     a max number of inserts into sets excluding bit sets (1024 by default)
  * @param allowRecursiveTypes    a flag that turns on support of recursive types (turned off by default)
  * @param requireDiscriminatorFirst a flag that turns off limitation for a position of the discriminator field to be
  *                               the first field of the JSON object (turned on by default)
  * @param useScalaEnumValueId    a flag that turns on using of ids for parsing and serialization of Scala enumeration
  *                               values
  */
class CodecMakerConfig(
    val fieldNameMapper: NameMapper,
    val javaEnumValueNameMapper: NameMapper,
    val adtLeafClassNameMapper: NameMapper,
    val discriminatorFieldName: Option[String],
    val isStringified: Boolean,
    val mapAsArray: Boolean,
    val skipUnexpectedFields: Boolean,
    val transientDefault: Boolean,
    val transientEmpty: Boolean,
    val transientNone: Boolean,
    val requireCollectionFields: Boolean,
    val bigDecimalPrecision: Int,
    val bigDecimalScaleLimit: Int,
    val bigDecimalDigitsLimit: Int,
    val bigIntDigitsLimit: Int,
    val bitSetValueLimit: Int,
    val mapMaxInsertNumber: Int,
    val setMaxInsertNumber: Int,
    val allowRecursiveTypes: Boolean,
    val requireDiscriminatorFirst: Boolean,
    val useScalaEnumValueId: Boolean) {

  @compileTimeOnly("withFieldNameMapper should be used only inside JsonCodec.make functions")
  def withFieldNameMapper(fieldNameMapper: PartialFunction[String, String]): CodecMakerConfig = ???

  @compileTimeOnly("withJavaEnumValueNameMapper should be used only inside JsonCodec.make functions")
  def withJavaEnumValueNameMapper(javaEnumValueNameMapper: PartialFunction[String, String]): CodecMakerConfig = ???

  @compileTimeOnly("withJavaEnumValueNameMapper should be used only inside JsonCodec.make functions")
  def withAdtLeafClassNameMapper(adtLeafClassNameMapper: String => String): CodecMakerConfig = ???

  def withDiscriminatorFieldName(discriminatorFieldName: Option[String]): CodecMakerConfig =
    copy(discriminatorFieldName = discriminatorFieldName)

  def withIsStringified(isStringified: Boolean): CodecMakerConfig = copy(isStringified = isStringified)

  def withMapAsArray(mapAsArray: Boolean): CodecMakerConfig = copy(mapAsArray = mapAsArray)

  def withSkipUnexpectedFields(skipUnexpectedFields: Boolean): CodecMakerConfig =
    copy(skipUnexpectedFields = skipUnexpectedFields)

  def withTransientDefault(transientDefault: Boolean): CodecMakerConfig = copy(transientDefault = transientDefault)

  def withTransientEmpty(transientEmpty: Boolean): CodecMakerConfig = copy(transientEmpty = transientEmpty)

  def withTransientNone(transientNone: Boolean): CodecMakerConfig = copy(transientNone = transientNone)

  def withRequireCollectionFields(requireCollectionFields: Boolean): CodecMakerConfig =
    copy(requireCollectionFields = requireCollectionFields)

  def withBigDecimalPrecision(bigDecimalPrecision: Int): CodecMakerConfig =
    copy(bigDecimalPrecision = bigDecimalPrecision)

  def withBigDecimalScaleLimit(bigDecimalScaleLimit: Int): CodecMakerConfig =
    copy(bigDecimalScaleLimit = bigDecimalScaleLimit)

  def withBigDecimalDigitsLimit(bigDecimalDigitsLimit: Int): CodecMakerConfig =
    copy(bigDecimalDigitsLimit = bigDecimalDigitsLimit)

  def withBigIntDigitsLimit(bigIntDigitsLimit: Int): CodecMakerConfig = copy(bigIntDigitsLimit = bigIntDigitsLimit)

  def withBitSetValueLimit(bitSetValueLimit: Int): CodecMakerConfig = copy(bitSetValueLimit = bitSetValueLimit)

  def withMapMaxInsertNumber(mapMaxInsertNumber: Int): CodecMakerConfig = copy(mapMaxInsertNumber = mapMaxInsertNumber)

  def withSetMaxInsertNumber(setMaxInsertNumber: Int): CodecMakerConfig = copy(setMaxInsertNumber = setMaxInsertNumber)

  def withAllowRecursiveTypes(allowRecursiveTypes: Boolean): CodecMakerConfig =
    copy(allowRecursiveTypes = allowRecursiveTypes)

  def withRequireDiscriminatorFirst(requireDiscriminatorFirst: Boolean): CodecMakerConfig =
    copy(requireDiscriminatorFirst = requireDiscriminatorFirst)

  def withUseScalaEnumValueId(useScalaEnumValueId: Boolean): CodecMakerConfig =
    copy(useScalaEnumValueId = useScalaEnumValueId)

  def copy(fieldNameMapper: NameMapper = fieldNameMapper,
           javaEnumValueNameMapper: NameMapper = javaEnumValueNameMapper,
           adtLeafClassNameMapper: NameMapper = adtLeafClassNameMapper,
           discriminatorFieldName: Option[String] = discriminatorFieldName,
           isStringified: Boolean = isStringified,
           mapAsArray: Boolean = mapAsArray,
           skipUnexpectedFields: Boolean = skipUnexpectedFields,
           transientDefault: Boolean = transientDefault,
           transientEmpty: Boolean = transientEmpty,
           transientNone: Boolean = transientNone,
           requireCollectionFields: Boolean = requireCollectionFields,
           bigDecimalPrecision: Int = bigDecimalPrecision,
           bigDecimalScaleLimit: Int = bigDecimalScaleLimit,
           bigDecimalDigitsLimit: Int = bigDecimalDigitsLimit,
           bigIntDigitsLimit: Int = bigIntDigitsLimit,
           bitSetValueLimit: Int = bitSetValueLimit,
           mapMaxInsertNumber: Int = mapMaxInsertNumber,
           setMaxInsertNumber: Int = setMaxInsertNumber,
           allowRecursiveTypes: Boolean = allowRecursiveTypes,
           requireDiscriminatorFirst: Boolean = requireDiscriminatorFirst,
           useScalaEnumValueId: Boolean = useScalaEnumValueId): CodecMakerConfig =
    new CodecMakerConfig(
      fieldNameMapper = fieldNameMapper,
      javaEnumValueNameMapper = javaEnumValueNameMapper,
      adtLeafClassNameMapper = adtLeafClassNameMapper,
      discriminatorFieldName = discriminatorFieldName,
      isStringified = isStringified,
      mapAsArray= mapAsArray,
      skipUnexpectedFields = skipUnexpectedFields,
      transientDefault = transientDefault,
      transientEmpty = transientEmpty,
      transientNone = transientNone,
      requireCollectionFields = requireCollectionFields,
      bigDecimalPrecision = bigDecimalPrecision,
      bigDecimalScaleLimit = bigDecimalScaleLimit,
      bigDecimalDigitsLimit = bigDecimalDigitsLimit,
      bigIntDigitsLimit = bigIntDigitsLimit,
      bitSetValueLimit = bitSetValueLimit,
      mapMaxInsertNumber = mapMaxInsertNumber,
      setMaxInsertNumber = setMaxInsertNumber,
      allowRecursiveTypes = allowRecursiveTypes,
      requireDiscriminatorFirst = requireDiscriminatorFirst,
      useScalaEnumValueId = useScalaEnumValueId)
}

object CodecMakerConfig extends CodecMakerConfig(
  fieldNameMapper = JsonCodecMaker.partialIdentity,
  javaEnumValueNameMapper = JsonCodecMaker.partialIdentity,
  adtLeafClassNameMapper = JsonCodecMaker.simpleClassName,
  discriminatorFieldName = Some("type"),
  isStringified = false,
  mapAsArray = false,
  skipUnexpectedFields = true,
  transientDefault = true,
  transientEmpty = true,
  transientNone = true,
  requireCollectionFields = false,
  bigDecimalPrecision = 34, // precision for decimal128: java.math.MathContext.DECIMAL128.getPrecision
  bigDecimalScaleLimit = 6178, // limit for scale for decimal128: BigDecimal("0." + "0" * 33 + "1e-6143", java.math.MathContext.DECIMAL128).scale + 1
  bigDecimalDigitsLimit = 308, // 128 bytes: (BigDecimal(BigInt("9" * 307))).underlying.unscaledValue.toByteArray.length
  bigIntDigitsLimit = 308, // 128 bytes: (BigInt("9" * 307)).underlying.toByteArray.length
  bitSetValueLimit = 1024, // 128 bytes: collection.mutable.BitSet(1023).toBitMask.length * 8
  mapMaxInsertNumber = 1024, // to limit attacks from untrusted input that exploit worst complexity for inserts
  setMaxInsertNumber = 1024, // to limit attacks from untrusted input that exploit worst complexity for inserts
  allowRecursiveTypes = false, // to avoid stack overflow errors with untrusted input
  requireDiscriminatorFirst = true, // to avoid CPU overuse when the discriminator appears in the end of JSON objects, especially nested
  useScalaEnumValueId = false) {

  /**
    * Use to enable printing of codec during compilation:
    *{{{
    *given CodecMakerConfig.PrintCodec with {}
    *val codec = JsonCodecMaker.make[MyClass]
    *}}}
    **/
  class PrintCodec

  /**
    * Use to print additional debug code during derivation of codecs:
    *{{{
    *given CodecMakerConfig.Trace with {}
    *val codec = JsonCodecMaker.make[MyClass]
    *}}}
    **/
  //class Trace

  given FromExpr[CodecMakerConfig] with {
    def extract[X: FromExpr](name: String, x: Expr[X])(using Quotes): X = {
      import quotes.reflect._

      summon[FromExpr[X]].unapply(x).getOrElse(throw FromExprException(s"Can't parse $name: ${x.show}, tree: ${x.asTerm}", x))
    }

    def unapply(x: Expr[CodecMakerConfig])(using Quotes): Option[CodecMakerConfig] = {
      import quotes.reflect._

      x match
        case '{
          CodecMakerConfig(
            $exprFieldNameMapper,
            $exprJavaEnumValueNameMapper,
            $exprAdtLeafClassNameMapper,
            $exprDiscriminatorFieldName,
            $exprIsStringified,
            $exprMapAsArray,
            $exprSkipUnexpectedFields,
            $exprTransientDefault,
            $exprTransientEmpty,
            $exprTransientNone,
            $exprRequireCollectionFields,
            $exprBigDecimalPrecision,
            $exprBigDecimalScaleLimit,
            $exprBigDecimalDigitsLimit,
            $exprBigIntDigitsLimit,
            $exprBitSetValueLimit,
            $exprMapMaxInsertNumber,
            $exprSetMaxInsertNumber,
            $exprAllowRecursiveTypes,
            $exprRequireDiscriminatorFirst,
            $exprUseScalaEnumValueId)
        } =>
          try {
            Some(CodecMakerConfig(
              extract("fieldNameMapper", exprFieldNameMapper),
              extract("javaEnumValueNameMapper", exprJavaEnumValueNameMapper),
              extract("eadtLeafClassNameMapper", exprAdtLeafClassNameMapper),
              extract("discriminatorFieldName", exprDiscriminatorFieldName),
              extract("isStringified", exprIsStringified),
              extract("mapAsArray", exprMapAsArray),
              extract("skipUnexpectedFields", exprSkipUnexpectedFields),
              extract("transientDefault", exprTransientDefault),
              extract("transientEmpty", exprTransientEmpty),
              extract("transientNone", exprTransientNone),
              extract("requireCollectionFields", exprRequireCollectionFields),
              extract("bigDecimalPrecision", exprBigDecimalPrecision),
              extract("bigDecimalScaleLimit", exprBigDecimalScaleLimit),
              extract("bigDecimalDigitsLimit", exprBigDecimalDigitsLimit),
              extract("bigIntDigitsLimit", exprBigIntDigitsLimit),
              extract("bitSetValueLimit", exprBitSetValueLimit),
              extract("mapMaxInsertNumber", exprMapMaxInsertNumber),
              extract("setMaxInsertNumber", exprSetMaxInsertNumber),
              extract("allowRecursiveTypes", exprAllowRecursiveTypes),
              extract("requireDiscriminatorFirst", exprRequireDiscriminatorFirst),
              extract("useScalaEnumValueId", exprUseScalaEnumValueId)))
          } catch {
            case FromExprException(message, expr) =>
              report.warning(message, expr)
              None
          }
        case '{ ($x: CodecMakerConfig).withAllowRecursiveTypes($v) } => Some(x.valueOrAbort.withAllowRecursiveTypes(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withDiscriminatorFieldName($v) } => Some(x.valueOrAbort.withDiscriminatorFieldName(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withUseScalaEnumValueId($v) } => Some(x.valueOrAbort.withUseScalaEnumValueId(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withIsStringified($v) } => Some(x.valueOrAbort.withIsStringified(v.valueOrAbort))
        case '{ CodecMakerConfig } => Some(CodecMakerConfig)
        case '{ ($x: CodecMakerConfig).withFieldNameMapper($v) } => Some(x.valueOrAbort.copy(fieldNameMapper = ExprPartialFunctionWrapper(v)))
        case '{ ($x: CodecMakerConfig).withJavaEnumValueNameMapper($v) } => Some(x.valueOrAbort.copy(javaEnumValueNameMapper = ExprPartialFunctionWrapper(v)))
        case '{ ($x: CodecMakerConfig).withAdtLeafClassNameMapper($v) } => Some(x.valueOrAbort.copy(adtLeafClassNameMapper = ExprPartialFunctionWrapper('{ { case x => $v(x) } })))
        case '{ ($x: CodecMakerConfig).withRequireDiscriminatorFirst($v) } => Some(x.valueOrAbort.copy(requireDiscriminatorFirst = v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withMapAsArray($v) } => Some(x.valueOrAbort.withMapAsArray(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withSkipUnexpectedFields($v) } => Some(x.valueOrAbort.withSkipUnexpectedFields(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withTransientDefault($v) } => Some(x.valueOrAbort.withTransientDefault(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withTransientEmpty($v) } => Some(x.valueOrAbort.withTransientEmpty(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withTransientNone($v) } => Some(x.valueOrAbort.withTransientNone(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withRequireCollectionFields($v) } => Some(x.valueOrAbort.withRequireCollectionFields(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withBigDecimalPrecision($v) } => Some(x.valueOrAbort.withBigDecimalPrecision(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withBigDecimalScaleLimit($v) } => Some(x.valueOrAbort.withBigDecimalScaleLimit(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withBigDecimalDigitsLimit($v) } => Some(x.valueOrAbort.withBigDecimalDigitsLimit(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withBigIntDigitsLimit($v) } => Some(x.valueOrAbort.copy(bigIntDigitsLimit = v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withBitSetValueLimit($v) } => Some(x.valueOrAbort.withBitSetValueLimit(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withMapMaxInsertNumber($v) } => Some(x.valueOrAbort.withMapMaxInsertNumber(v.valueOrAbort))
        case '{ ($x: CodecMakerConfig).withSetMaxInsertNumber($v) } => Some(x.valueOrAbort.withSetMaxInsertNumber(v.valueOrAbort))
        case other =>
          report.error(s"Can't interpret ${other.show} as a constant expression, tree=$other")
          None
    }
  }
}

object JsonCodecMaker {
  /**
    * A partial function that is a total in fact and always returns a string passed to it.
    *
    * @return a provided value
    */
  val partialIdentity: PartialFunction[String, String] = { case s => s }

  /**
    * Mapping function for field or class names that should be in camelCase format.
    *
    * @return a transformed name or the same name if no transformation is required
    */
  val enforceCamelCase: PartialFunction[String, String] = { case s => enforceCamelOrPascalCase(s, toPascal = false) }

  /**
    * Mapping function for field or class names that should be in PascalCase format.
    *
    * @return a transformed name or the same name if no transformation is required
    */
  val EnforcePascalCase: PartialFunction[String, String] = { case s => enforceCamelOrPascalCase(s, toPascal = true) }

  /**
    * Mapping function for field or class names that should be in snake_case format.
    *
    * @return a transformed name or the same name if no transformation is required
    */
  val enforce_snake_case: PartialFunction[String, String] = { case s => enforceSnakeOrKebabCase(s, '_') }

  /**
    * Mapping function for field or class names that should be in kebab-case format.
    *
    * @return a transformed name or the same name if no transformation is required
    */
  val `enforce-kebab-case`: PartialFunction[String, String] = { case s => enforceSnakeOrKebabCase(s, '-') }

  private[this] def enforceCamelOrPascalCase(s: String, toPascal: Boolean): String =
    if (s.indexOf('_') == -1 && s.indexOf('-') == -1) {
      if (s.isEmpty) s
      else {
        val ch = s.charAt(0)
        val fixedCh =
          if (toPascal) toUpperCase(ch)
          else toLowerCase(ch)
        s"$fixedCh${s.substring(1)}"
      }
    } else {
      val len = s.length
      val sb = new StringBuilder(len)
      var i = 0
      var isPrecedingDash = toPascal
      while (i < len) isPrecedingDash = {
        val ch = s.charAt(i)
        i += 1
        (ch == '_' || ch == '-') || {
          val fixedCh =
            if (isPrecedingDash) toUpperCase(ch)
            else toLowerCase(ch)
          sb.append(fixedCh)
          false
        }
      }
      sb.toString
    }

  private[this] def enforceSnakeOrKebabCase(s: String, separator: Char): String =
    val len = s.length
    val sb = new StringBuilder(len << 1)
    var i = 0
    var isPrecedingLowerCased = false
    while (i < len) isPrecedingLowerCased = {
      val ch = s.charAt(i)
      i += 1
      if (ch == '_' || ch == '-') {
        sb.append(separator)
        false
      } else if (isLowerCase(ch)) {
        sb.append(ch)
        true
      } else {
        if (isPrecedingLowerCased || i > 1 && i < len && isLowerCase(s.charAt(i))) sb.append(separator)
        sb.append(toLowerCase(ch))
        false
      }
    }
    sb.toString

  /**
    * Mapping function for class names that should be trimmed to the simple class name without package prefix.
    *
    * @param fullClassName the name to transform
    * @return a transformed name or the same name if no transformation is required
    */
  def simpleClassName(fullClassName: String): String =
    val lastComponent = fullClassName.substring(Math.max(fullClassName.lastIndexOf('.') + 1, 0))
    var localPrefixIndex = 0
    while (lastComponent.startsWith("_$", localPrefixIndex)) localPrefixIndex += 2
    lastComponent.substring(localPrefixIndex)

  /**
    * Derives a codec for JSON values for the specified type `A`.
    *
    * @tparam A a type that should be encoded and decoded by the derived codec
    * @return an instance of the derived codec
    */
  inline def make[A]: JsonValueCodec[A] = ${Impl.makeWithDefaultConfig[A]}

  /**
    * A replacement for the `make` call with the `CodecMakerConfig.withDiscriminatorFieldName(None)` configuration
    * parameter.
    *
    * @tparam A a type that should be encoded and decoded by the derived codec
    * @return an instance of the derived codec
    */
  inline def makeWithoutDiscriminator[A]: JsonValueCodec[A] = ${Impl.makeWithoutDiscriminator[A]}

  /**
    * A replacement for the `make` call with the
    * `CodecMakerConfig.withTransientEmpty(false).withRequireCollectionFields(true)`
    * configuration parameter.
    *
    * @tparam A a type that should be encoded and decoded by the derived codec
    * @return an instance of the derived codec
    */
  inline def makeWithRequiredCollectionFields[A]: JsonValueCodec[A] = ${Impl.makeWithRequiredCollectionFields[A]}

  /**
    * A replacement for the `make` call with the
    * `CodecMakerConfig.withTransientEmpty(false).withRequireCollectionFields(true).withDiscriminatorFieldName(Some("name"))`
    * configuration parameter.
    *
    * @tparam A a type that should be encoded and decoded by the derived codec
    * @return an instance of the derived codec
    */
  inline def makeWithRequiredCollectionFieldsAndNameAsDiscriminatorFieldName[A]: JsonValueCodec[A] =
    ${Impl.makeWithRequiredCollectionFieldsAndNameAsDiscriminatorFieldName[A]}

  /**
    * Derives a codec for JSON values for the specified type `A` and a provided derivation configuration.
    *
    * @param config a derivation configuration
    * @tparam A a type that should be encoded and decoded by the derived codec
    * @return an instance of the derived codec
    */
  inline def make[A](inline config: CodecMakerConfig): JsonValueCodec[A] = ${Impl.makeWithSpecifiedConfig[A]('config)}

  private[macros] object Impl {
    def makeWithDefaultConfig[A: Type](using Quotes): Expr[JsonValueCodec[A]] = tryMake(CodecMakerConfig)

    def makeWithoutDiscriminator[A: Type](using Quotes): Expr[JsonValueCodec[A]] =
      tryMake(CodecMakerConfig.withDiscriminatorFieldName(None))

    def makeWithRequiredCollectionFields[A: Type](using Quotes): Expr[JsonValueCodec[A]] =
      tryMake(CodecMakerConfig.withTransientEmpty(false).withRequireCollectionFields(true))

    def makeWithRequiredCollectionFieldsAndNameAsDiscriminatorFieldName[A: Type](using Quotes): Expr[JsonValueCodec[A]] =
      tryMake(CodecMakerConfig.withTransientEmpty(false).withRequireCollectionFields(true)
        .withDiscriminatorFieldName(Some("name")))

    def makeWithSpecifiedConfig[A: Type](config: Expr[CodecMakerConfig])(using Quotes): Expr[JsonValueCodec[A]] = {
      import quotes.reflect._

      tryMake[A](summon[FromExpr[CodecMakerConfig]].unapply(config)
        .fold(report.errorAndAbort(s"Cannot evaluate a parameter of the 'make' macro call for type '${Type.show[A]}'. ")) {
          cfg =>
            if (cfg.requireCollectionFields && cfg.transientEmpty)
              report.errorAndAbort("'requireCollectionFields' and 'transientEmpty' cannot be 'true' simultaneously")
            cfg
        })
    }

    private[this] def tryMake[A: Type](cfg: CodecMakerConfig)(using Quotes): Expr[JsonValueCodec[A]] = {
      import quotes.reflect._

      try make[A](cfg) catch {
        case ex: quoted.runtime.StopMacroExpansion => throw ex
        case ex: CompileTimeEvalException => report.errorAndAbort("Can't evaluate compile-time expression", ex.expr)
        case NonFatal(ex) =>
          if (false/*Expr.summon[CodecMakerConfig.Trace].isDefined*/) {
            println(s"Catched exception during macro expansion: $ex: msg=${ex.getMessage}")
            ex.printStackTrace()
          }
          throw ex
      }
    }

    private[this] def make[A: Type](cfg: CodecMakerConfig)(using Quotes): Expr[JsonValueCodec[A]] = {
      import quotes.reflect._

      val traceFlag: Boolean = false //Expr.summon[CodecMakerConfig.Trace].isDefined

      def fail(msg: String): Nothing = report.errorAndAbort(msg, Position.ofMacroExpansion)

      def warn(msg: String): Unit = report.warning(msg, Position.ofMacroExpansion)

      def typeArgs(tpe: TypeRepr): List[TypeRepr] = tpe match
        case AppliedType(_, typeArgs) => typeArgs.map(_.dealias)
        case _ => Nil

      def typeArg1(tpe: TypeRepr): TypeRepr = typeArgs(tpe).head

      def typeArg2(tpe: TypeRepr): TypeRepr = typeArgs(tpe).tail.head

      def isTuple(tpe: TypeRepr): Boolean = tpe <:< TypeRepr.of[Tuple]

      def isValueClass(tpe: TypeRepr): Boolean = !isConstType(tpe) && tpe <:< TypeRepr.of[AnyVal]

      def valueClassValue(tpe: TypeRepr): Symbol = tpe.typeSymbol.fieldMembers(0)

      def substituteTypes(tpe: TypeRepr, from: List[Symbol], to: List[TypeRepr]): TypeRepr = {
        // origin substitute-types in @experimantal annotations
        // this will be enabled when feature will no longer experimental (scala 3.2 ?)
        // import scala.language.experimental
        // try tpe.substituteTypes(from, to) catch { case NonFatal(_) =>
        //   fail(s"Cannot resolve generic type(s) for `$tpe`. Please provide a custom implicitly accessible codec for it.")
        // }
        val symTypeMap = from.zip(to).toMap

        def substituteMap(tpe: TypeRepr): TypeRepr = tpe match
          case ConstantType(_) => tpe
          case TermRef(repr, name) => TermRef(substituteMap(repr), name)
          case ti@TypeRef(_, _) =>
            if (ti.typeSymbol.isTypeParam) symTypeMap.get(ti.typeSymbol).getOrElse(tpe)
            else tpe // TypRef have no unapply, hope for the best
          case SuperType(thisTpe, superTpe) => SuperType(substituteMap(thisTpe), substituteMap(superTpe))
          case Refinement(parent, name, info) => Refinement(substituteMap(parent), name, substituteMap(info))
          case AppliedType(base, typeArgs) => substituteMap(base).appliedTo(typeArgs.map(substituteMap))
          case AnnotatedType(underlying, annotated) => AnnotatedType(substituteMap(underlying), annotated)
          case AndType(rhs, lhs) => AndType(substituteMap(rhs), substituteMap(lhs))
          case OrType(rhs, lhs) => OrType(substituteMap(rhs), substituteMap(lhs))
          case MatchType(bound, scrutinee, cases) =>
            MatchType(substituteMap(bound), substituteMap(scrutinee), cases.map(substituteMap))
          case ByNameType(underlying) => ByNameType(substituteMap(underlying))
          case tl@TypeLambda(names, bounds, body) =>
            var paramValues = Map.empty[Symbol, TypeRepr]
            names.zipWithIndex.foreach { case (n, i) =>
              val isym = tl.param(i).typeSymbol
              symTypeMap.get(isym).foreach(tree => paramValues = paramValues.updated(isym, tree))
            }
            if (paramValues.size == names.size) substituteMap(body)
            else if (paramValues.isEmpty) TypeLambda(names, _ => bounds, _ => substituteMap(body))
            else fail(s"Partial type lambda applications are not suported for type: ${tpe.show}")
          case r: RecursiveType => fail(s"Recurive types are not supported, use a custom implicitly accessible " +
              s"codec (${r.show} during transform of ${tpe.show})")
          case l: LambdaType => fail(s"Lambda types are not supported, use a custom implicitly accessible codec " +
              s"(${l.show} during transform of ${tpe.show})")

        substituteMap(tpe)
      }

      def valueClassValueType(tpe: TypeRepr): TypeRepr = tpe.memberType(tpe.typeSymbol.fieldMembers(0)).dealias

      def isNonAbstractScalaClass(tpe: TypeRepr): Boolean = tpe.classSymbol.fold(false) { sym =>
        val flags = sym.flags
        !flags.is(Flags.Abstract) && !flags.is(Flags.JavaDefined) && !flags.is(Flags.Trait)
      }

      def isSealedClass(tpe: TypeRepr): Boolean = tpe.typeSymbol.flags.is(Flags.Sealed)

      def isConstType(tpe: TypeRepr): Boolean = tpe match
        case ConstantType(_) => true
        case _ => false

      def isEnumOrModuleValue(tpe: TypeRepr): Boolean =
        tpe.isSingleton && (tpe.typeSymbol.flags.is(Flags.Enum) || tpe.typeSymbol.flags.is(Flags.Module))

      def getEnclosingClass(sym: Symbol): Symbol =
        if (sym.isClassDef) sym
        else getEnclosingClass(sym.owner)

      def adtChildren(tpe: TypeRepr): Seq[TypeRepr] = { // TODO: explore yet one variant with mirrors
        def resolveParentTypeArg(child: Symbol, fromNudeChildTarg: TypeRepr, parentTarg: TypeRepr,
                                 binding: Map[String, TypeRepr]): Map[String, TypeRepr] =
          if (fromNudeChildTarg.typeSymbol.isTypeParam) { // todo: check for paramRef instead ?
            val paramName = fromNudeChildTarg.typeSymbol.name
            binding.get(paramName) match
              case None => binding.updated(paramName, parentTarg)
              case Some(oldBinding) =>
                if (oldBinding =:= parentTarg) binding
                else fail(s"Type parameter $paramName in class ${child.name} appeared in the constructor of " +
                  s"${tpe.show} two times differently, with ${oldBinding.show} and ${parentTarg.show}")
          } else if (fromNudeChildTarg <:< parentTarg) binding // TODO: assupe parentTag is covariant, get covariance from tycon type parameters.
          else {
            (fromNudeChildTarg, parentTarg) match
              case (AppliedType(ctycon, ctargs), AppliedType(ptycon, ptargs)) =>
                ctargs.zip(ptargs).foldLeft(resolveParentTypeArg(child, ctycon, ptycon, binding)) { (b, e) =>
                  resolveParentTypeArg(child, e._1, e._2, b)
                }
              case other => fail(s"Failed unification of type parameters of ${tpe.show} from child $child - " +
                  s"${fromNudeChildTarg.show} and ${parentTarg.show}")
          }

        def resolveParentTypeArgs(child: Symbol, nudeChildParentTags: List[TypeRepr], parentTags: List[TypeRepr],
                                  binding: Map[String, TypeRepr]): Map[String, TypeRepr] =
          nudeChildParentTags.zip(parentTags).foldLeft(binding)((s, e) => resolveParentTypeArg(child, e._1, e._2, s))

        def isHK(ctArgs: List[TypeRepr]): Boolean = ctArgs.exists {
          case TypeLambda(_, _, _) => true
          case _ => false
        }

        tpe.typeSymbol.children.map { sym =>
          if (sym.isType) {
            if (sym.name == "") // problem - we have no other way to find this other return the name
              fail(s"Local child symbols are not supported, please consider change '${tpe.show}' or implement a " +
                "custom implicitly accessible codec")
            val nudeSubtype = TypeIdent(sym).tpe
            val tpeArgsFromChild = typeArgs(nudeSubtype.baseType(tpe.typeSymbol))
            nudeSubtype.memberType(sym.primaryConstructor) match
              case MethodType(_, _, resTp) => resTp
              case PolyType(names, bounds, resPolyTp) =>
                val targs = typeArgs(tpe)
                val tpBinding = resolveParentTypeArgs(sym, tpeArgsFromChild, targs, Map.empty)
                val ctArgs = names.map { name =>
                  tpBinding.get(name)
                    .getOrElse(fail(s"Type parameter $name of $sym can't be deduced from type arguments of " +
                      s"${tpe.show}. Please provide a custom implicitly accessible codec for if"))
                }
                val polyRes = resPolyTp match
                  case MethodType(_, _, resTp) => resTp
                  case other => other // hope we have no multiple typed param lists yet.
                if (ctArgs.isEmpty) polyRes
                else polyRes match
                  case AppliedType(base, _) => base.appliedTo(ctArgs)
                  case AnnotatedType(AppliedType(base, _), annot) => AnnotatedType(base.appliedTo(ctArgs), annot)
                  case _ => polyRes.appliedTo(ctArgs)
              case other => fail(s"Primary constructior for ${tpe.show} is not MethodType or PolyType but $other")
          } else if (sym.isTerm) Ref(sym).tpe
          else fail("Only Scala classes & objects are supported for ADT leaf classes. Please consider using of " +
            s"them for ADT with base '${tpe.show}' or provide a custom implicitly accessible codec for the ADT base. " +
            s"Failed symbol: $sym (fullName=${sym.fullName})\n")
        }
      }

      def adtLeafClasses(adtBaseTpe: TypeRepr): Seq[TypeRepr] = {
        def collectRecursively(tpe: TypeRepr): Seq[TypeRepr] =
          val leafTpes = adtChildren(tpe).flatMap { subTpe =>
            if (isEnumOrModuleValue(subTpe)) subTpe :: Nil
            else if (isSealedClass(subTpe)) collectRecursively(subTpe)
            else if (isNonAbstractScalaClass(subTpe)) subTpe :: Nil
            else fail(if (subTpe.typeSymbol.flags.is(Flags.Abstract) || subTpe.typeSymbol.flags.is(Flags.Trait) ) {
              "Only sealed intermediate traits or abstract classes are supported. Please consider using of them " +
                s"for ADT with base '${adtBaseTpe.show}' or provide a custom implicitly accessible codec for the ADT base."
            } else {
              "Only Scala classes & objects are supported for ADT leaf classes. Please consider using of them " +
                s"for ADT with base '${adtBaseTpe.show}' or provide a custom implicitly accessible codec for the ADT base."
            })
          }
          if (isNonAbstractScalaClass(tpe)) leafTpes :+ tpe
          else leafTpes

        val classes = collectRecursively(adtBaseTpe).distinct
        if (classes.isEmpty) fail(s"Cannot find leaf classes for ADT base '${adtBaseTpe.show}'. " +
          "Please add them or provide a custom implicitly accessible codec for the ADT base.")
        classes
      }

      def companion(tpe: TypeRepr): Symbol = tpe.typeSymbol.moduleClass

      def isOption(tpe: TypeRepr): Boolean = tpe <:< TypeRepr.of[Option[_]]

      def isCollection(tpe: TypeRepr): Boolean = tpe <:< TypeRepr.of[Iterable[_]] || tpe <:< TypeRepr.of[Array[_]]

      def isJavaEnum(tpe: TypeRepr): Boolean = tpe <:< TypeRepr.of[java.lang.Enum[_]]

      def scalaCollectionCompanion(tpe: TypeRepr): Term =
        if (tpe.typeSymbol.fullName.startsWith("scala.collection.")) Ref(tpe.typeSymbol.companionModule)
        else fail(s"Unsupported type '${tpe.show}'. Please consider using a custom implicitly accessible codec for it.")

      def scalaCollectionEmptyNoArgs(cTpe: TypeRepr, eTpe: TypeRepr): Term =
        TypeApply(Select.unique(scalaCollectionCompanion(cTpe), "empty"), List(Inferred(eTpe)))

      def scalaMapEmptyNoArgs(cTpe: TypeRepr, kTpe: TypeRepr, vTpe: TypeRepr): Term =
        TypeApply(Select.unique(scalaCollectionCompanion(cTpe), "empty"), List(Inferred(kTpe), Inferred(vTpe)))

      def scala2EnumerationObject(tpe: TypeRepr): Expr[Enumeration] = tpe match
        case TypeRef(ct, _) if ct.isSingleton => Ref(ct.termSymbol).asExprOf[Enumeration]
        case _ => fail(s"For Scala 2 enum type reference to singleton term is expected, we have ${tpe.show}")

      def summonOrdering(tpe: TypeRepr): Term = tpe.asType match
        case '[t] => Expr.summon[Ordering[t]].fold(fail(s"Can't summon Ordering[${tpe.show}]"))(_.asTerm)

      def summonClassTag(tpe: TypeRepr): Term = tpe.asType match
        case '[t] => Expr.summon[ClassTag[t]].fold(fail(s"Can't summon ClassTag[${tpe.show}]"))(_.asTerm)

      def findScala2EnumerationById[C <: AnyRef: Type](tpe: TypeRepr, i: Expr[Int])(using Quotes): Expr[Option[C]] =
        '{ ${scala2EnumerationObject(tpe)}.values.iterator.find(_.id == $i) }.asExprOf[Option[C]]

      def findScala2EnumerationByName[C <: AnyRef: Type](tpe: TypeRepr, name: Expr[String])(using Quotes): Expr[Option[C]] =
        '{ ${scala2EnumerationObject(tpe)}.values.iterator.find(_.toString == $name) }.asExprOf[Option[C]]

      val rootTpe = TypeRepr.of[A].dealias
      val inferredKeyCodecs = mutable.Map.empty[TypeRepr, Option[Expr[JsonKeyCodec[_]]]]
      val inferredValueCodecs = mutable.Map.empty[TypeRepr, Option[Expr[JsonValueCodec[_]]]]

      def inferImplicitValue(typeToSearch: TypeRepr): Option[Term] = Implicits.search(typeToSearch) match
        case v: ImplicitSearchSuccess => Some(v.tree)
        case _ => None

      def symbol(name: String, tpe: TypeRepr, flags: Flags = Flags.EmptyFlags): Symbol =
        Symbol.newVal(Symbol.spliceOwner, name, tpe, flags, Symbol.noSymbol)

      def checkRecursionInTypes(types: List[TypeRepr]): Unit =
        if (!cfg.allowRecursiveTypes) {
          val tpe :: nested = types
          if (!tpe.typeSymbol.flags.is(Flags.Enum)) {
            val recursiveIdx = nested.indexOf(tpe)
            if (recursiveIdx >= 0) {
              val recTypes = nested.take(recursiveIdx + 1).map(_.show).reverse.mkString("'", "', '", "'")
              fail(s"Recursive type(s) detected: $recTypes. Please consider using a custom implicitly " +
                s"accessible codec for this type to control the level of recursion or turn on the " +
                s"'${Type.show[CodecMakerConfig]}.allowRecursiveTypes' for the trusted input that " +
                s"will not exceed the thread stack size.\nall types: ${types.map(_.show)}")
            }
          }
        }

      def findImplicitKeyCodec(types: List[TypeRepr]): Option[Expr[JsonKeyCodec[_]]] =
        val tpe :: nestedTypes = types
        if (nestedTypes.isEmpty) None
        else {
          checkRecursionInTypes(types)
          if (tpe =:= rootTpe) None
          else inferredKeyCodecs.getOrElseUpdate(tpe, {
            inferImplicitValue(TypeRepr.of[JsonKeyCodec].appliedTo(tpe)).map(_.asExprOf[JsonKeyCodec[_]])
          })
        }

      def findImplicitValueCodec(types: List[TypeRepr]): Option[Expr[JsonValueCodec[_]]] =
        val tpe :: nestedTypes = types
        if (nestedTypes.isEmpty) None
        else {
          checkRecursionInTypes(types)
          if (tpe =:= rootTpe) None
          else inferredValueCodecs.getOrElseUpdate(tpe, {
            inferImplicitValue(TypeRepr.of[JsonValueCodec].appliedTo(tpe)).map(_.asExprOf[JsonValueCodec[_]])
          })
        }

      val mathContexts = new mutable.LinkedHashMap[Int, ValDef]

      def withMathContextFor(precision: Int): Expr[MathContext] =
        if (precision == MathContext.DECIMAL128.getPrecision) '{ MathContext.DECIMAL128 }
        else if (precision == MathContext.DECIMAL64.getPrecision) '{ MathContext.DECIMAL64 }
        else if (precision == MathContext.DECIMAL32.getPrecision) '{ MathContext.DECIMAL32 }
        else if (precision == MathContext.UNLIMITED.getPrecision) '{ MathContext.UNLIMITED }
        else Ref(mathContexts.getOrElseUpdate(precision, {
          val mc = '{ new MathContext(${Expr(cfg.bigDecimalPrecision)}, java.math.RoundingMode.HALF_EVEN) }
          val sym = symbol("mc" + mathContexts.size, TypeRepr.of[MathContext])
          ValDef(sym, Some(mc.asTerm.changeOwner(sym)))
        }).symbol).asExprOf[MathContext]

      val scala2EnumerationCaches = new mutable.LinkedHashMap[TypeRepr, ValDef]

      def withScala2EnumerationConcurrentCacheFor[K: Type, T: Type](tpe: TypeRepr)(using Quotes): Expr[ConcurrentHashMap[K, T]] =
        Ref(scala2EnumerationCaches.getOrElseUpdate(tpe, {
          val sym = symbol("ec" + scala2EnumerationCaches.size, TypeRepr.of[ConcurrentHashMap[K, T]])
          ValDef(sym, Some('{ new ConcurrentHashMap[K, T] }.asTerm.changeOwner(sym)))
        }).symbol).asExprOf[ConcurrentHashMap[K, T]]

      def withScala2EnumerationSingleThreadCacheFor[K: Type, T: Type](tpe: TypeRepr)(using Quotes): Expr[java.util.HashMap[K, T]] =
        Ref(scala2EnumerationCaches.getOrElseUpdate(tpe, {
          val sym = symbol("ec" + scala2EnumerationCaches.size, TypeRepr.of[java.util.HashMap[K, T]])
          ValDef(sym, Some('{ new java.util.HashMap[K, T] }.asTerm.changeOwner(sym)))
        }).symbol).asExprOf[java.util.HashMap[K, T]]

      case class JavaEnumValueInfo(value: Symbol, name: String, transformed: Boolean)

      val javaEnumValueInfos = new mutable.LinkedHashMap[TypeRepr, Seq[JavaEnumValueInfo]]

      def javaEnumValues(tpe: TypeRepr): Seq[JavaEnumValueInfo] = javaEnumValueInfos.getOrElseUpdate(tpe, {
        val classSym = tpe.classSymbol.getOrElse(fail(s"$tpe is not a class"))
        val values = classSym.children.map { sym =>
          val name = sym.name
          val transformed = cfg.javaEnumValueNameMapper(name).getOrElse(name)
          JavaEnumValueInfo(sym, transformed, name != transformed)
        }
        val nameCollisions = duplicated(values.map(_.name))
        if (nameCollisions.nonEmpty) {
          val formattedCollisions = nameCollisions.mkString("'", "', '", "'")
          fail(s"Duplicated JSON value(s) defined for '${tpe.show}': $formattedCollisions. Values are " +
            s"derived from value names of the enum that are mapped by the '${Type.show[CodecMakerConfig]}" +
            s".javaEnumValueNameMapper' function. Result values should be unique per enum class. All names: $values")
        }
        values
      })

      def genReadJavaEnumValue[E: Type](enumValues: Seq[JavaEnumValueInfo], unexpectedEnumValueHandler: Expr[E],
                                        in: Expr[JsonReader], l: Expr[Int])(using Quotes): Expr[E] = {
        def genReadCollisions(es: collection.Seq[JavaEnumValueInfo]): Expr[E] =
          es.foldRight(unexpectedEnumValueHandler) { (e, acc) => '{
            if ($in.isCharBufEqualsTo($l, ${Expr(e.name)})) ${Ref(e.value).asExprOf[E]}
            else $acc
          } }

        if (enumValues.size <= 8 && enumValues.foldLeft(0)(_ + _.name.length) <= 64) genReadCollisions(enumValues)
        else {
          val hashCode = (e: JavaEnumValueInfo) => JsonReader.toHashCode(e.name.toCharArray, e.name.length)
          val cases = groupByOrdered(enumValues)(hashCode).map { case (hash, fs) =>
            val sym = Symbol.newBind(Symbol.spliceOwner, "b" + hash, Flags.EmptyFlags, TypeRepr.of[Int])
            CaseDef(Bind(sym, Expr(hash).asTerm), None, genReadCollisions(fs).asTerm)
          } :+ CaseDef(Wildcard(), None, unexpectedEnumValueHandler.asTerm)
          Match('{ $in.charBufToHashCode($l) }.asTerm, cases.toList).asExprOf[E]
        }
      }

      case class FieldInfo(symbol: Symbol,
                           mappedName: String,
                           getterOrField: FieldInfo.GetterOrField,
                           defaultValue: Option[Term],
                           resolvedTpe: TypeRepr,
                           isTransient: Boolean,
                           isStringified: Boolean,
                           nonTransientFieldIndex: Int) {
        def genGet(obj: Term): Term = getterOrField match
          case FieldInfo.Getter(getter) => Select(obj, getter)
          case FieldInfo.Field(field) => Select(obj, field)
          case FieldInfo.NoField => fail(s"Getter is called for $mappedName") // TODO: better description

        def optTypeSelect(obj: Term, fieldOrGetter: Symbol): Term = typeArgs(obj.tpe) match
          case Nil => Select(obj, fieldOrGetter)
          case typeArgs => TypeApply(Select(obj, fieldOrGetter), typeArgs.map(Inferred(_)))
      }

      object FieldInfo {
        sealed trait GetterOrField

        case class Getter(symbol: Symbol) extends GetterOrField

        case class Field(symbol: Symbol) extends GetterOrField

        case object NoField extends GetterOrField
      }

      case class ClassInfo(tpe: TypeRepr, primaryConstructor: Symbol, allFields: IndexedSeq[FieldInfo]) {
        def nonTransientFields: Seq[FieldInfo] = allFields.filter(!_.isTransient)

        def genNew(args: List[Term]): Term =
          if (!primaryConstructor.isClassConstructor) fail(s"Cannot generate new for ${tpe.show}")
          val constructorNoTypes = Select(New(Inferred(tpe)), primaryConstructor)
          val constructor = typeArgs(tpe) match
            case Nil => constructorNoTypes
            case typeArgs => TypeApply(constructorNoTypes, typeArgs.map(Inferred(_)))
          Apply(constructor, args)
      }

      val classInfos = new mutable.LinkedHashMap[TypeRepr, ClassInfo]

      def getClassInfo(tpe: TypeRepr): ClassInfo = classInfos.getOrElseUpdate(tpe, {
        case class FieldAnnotations(partiallyMappedName: String, transient: Boolean, stringified: Boolean)

        def getPrimaryConstructor(tpe: TypeRepr): Symbol = tpe.classSymbol match
          case Some(sym) if sym.primaryConstructor.exists => sym.primaryConstructor
          case _ => fail(s"Cannot find a primary constructor for '$tpe'")

        def hasSupportedAnnotation(m: Symbol): Boolean =
          m.annotations.exists(a => a.tpe <:< TypeRepr.of[named] || a.tpe <:< TypeRepr.of[transient] ||
            a.tpe <:< TypeRepr.of[stringified])

        val tpeClassSym = tpe.classSymbol.getOrElse(fail(s"Expected that ${tpe.show} has classSymbol"))
        val fieldsWithDefaultValues = tpeClassSym.fieldMembers.filter(_.flags.is(Flags.HasDefault))
        val annotations = tpeClassSym.fieldMembers.collect { case m: Symbol if hasSupportedAnnotation(m) =>
          val name = m.name
          val named = m.annotations.filter(_.tpe =:= TypeRepr.of[named])
          if (named.size > 1) fail(s"Duplicated '${TypeRepr.of[named].show}' defined for '$name' of '${tpe.show}'.")
          val trans = m.annotations.filter(_.tpe =:= TypeRepr.of[transient])
          if (trans.size > 1) warn(s"Duplicated '${TypeRepr.of[transient].show}' defined for '$name' of '${tpe.show}'.")
          val strings = m.annotations.filter(_.tpe =:= TypeRepr.of[stringified])
          if (strings.size > 1) warn(s"Duplicated '${TypeRepr.of[stringified].show}' defined for '$name' of '${tpe.show}'.")
          if ((named.nonEmpty || strings.nonEmpty) && trans.size == 1)
            warn(s"Both '${Type.show[transient]}' and '${Type.show[named]}' or " +
              s"'${Type.show[transient]}' and '${Type.show[stringified]}' defined for '$name' of '${tpe.show}'.")
          val partiallyMappedName = namedValueOpt(named.headOption, tpe).getOrElse(name)
          (name, FieldAnnotations(partiallyMappedName, trans.nonEmpty, strings.nonEmpty))
        }.toMap
        val primaryConstructor = getPrimaryConstructor(tpe)

        def createFieldInfos(params: List[Symbol], typeParams: List[Symbol]): IndexedSeq[FieldInfo] = {
          val fieldInfos = new ArrayBuffer[FieldInfo]
          var nonTransientFieldIndex = 0
          params.zipWithIndex.foreach { case (symbol, i) =>
            val name = symbol.name
            val annotationOption = annotations.get(name)
            val mappedName = annotationOption.fold(cfg.fieldNameMapper(name).getOrElse(name))(_.partiallyMappedName)
            val field = tpeClassSym.fieldMember(name)
            val getterOrField =
              if (field.exists) {
                if (field.flags.is(Flags.PrivateLocal)) fail(s"Field '$name' in class '${tpe.show}' is private. " +
                  "It should be defined as 'val' or 'var' in the primary constructor.")
                FieldInfo.Field(field)
              } else {
                val getters = tpeClassSym.methodMember(name)
                  .filter(_.flags.is(Flags.CaseAccessor | Flags.FieldAccessor | Flags.ParamAccessor))
                if (getters.isEmpty) { // Scala3 doesn't set FieldAccess flag for val parameters of constructor
                  val namedMembers = tpeClassSym.methodMember(name).filter(_.paramSymss == Nil)
                  if (namedMembers.isEmpty) fail(s"Field and getter not found: '$name' parameter of '${tpe.show}' " +
                    s"should be defined as 'val' or 'var' in the primary constructor.")
                  namedMembers.head.privateWithin match
                    case None => FieldInfo.Getter(namedMembers.head)
                    case _ => fail(s"Getter is private: '$name' paramter of '${tpe.show}' should be defined " +
                        "as 'val' or 'var' in the primary constructor.")
                } else FieldInfo.Getter(getters.head) // TODO: check length ?  when we have both reader and writer getters.filter(_.paramSymss == List(List()))
              }
            val defaultValue =
              if (symbol.flags.is(Flags.HasDefault)) {
                val dvMembers = tpe.typeSymbol.companionClass.methodMember("$lessinit$greater$default$" + (i + 1))
                if (dvMembers.isEmpty) fail(s"Can't find default value for $symbol in class ${tpe.show}")
                val methodSymbol = dvMembers.head
                val dvSelectNoTArgs = Ref(tpe.typeSymbol.companionModule).select(methodSymbol)
                val dvSelect = methodSymbol.paramSymss match
                  case Nil => dvSelectNoTArgs
                  case List(params) if (params.exists(_.isTypeParam)) => typeArgs(tpe) match
                    case Nil => fail(s"Expected that ${tpe.show} is an applied type")
                    case typeArgs => TypeApply(dvSelectNoTArgs, typeArgs.map(Inferred(_)))
                  case _ => fail(s"Default method for ${symbol.name} of class ${tpe.show} have a complex " +
                    s"parameter list: ${methodSymbol.paramSymss}")
                Some(dvSelect)
              } else None
            val isStringified = annotationOption.exists(_.stringified)
            val isTransient = annotationOption.exists(_.transient)
            val originFieldType = tpe.memberType(symbol).dealias
            val fieldType = typeArgs(tpe) match
              case Nil => originFieldType
              case typeArgs =>
                if (typeArgs.length != typeParams.length) // FIXME: here we assume that type-params for the primary constructor are the same as class type params
                  fail("Length of type-parameters of an aplied type and type parameters of primiary " +
                    s"constructors are different for ${tpe.show}")
                substituteTypes(originFieldType, typeParams, typeArgs)
            fieldType match
              case tl@TypeLambda(_, _, _) => fail(s"Hight-kinded types are not supported for type ${tpe.show} " +
                  s"with field type for '$name' (symbol=$symbol) : ${fieldType.show}, originFieldType=" +
                  s"${originFieldType.show}, constructor typeParams=$typeParams, ")
              case TypeBounds(_, _) => fail(s"Type bounds are not supported for type '${tpe.show}' with field " +
                  s"type for $name '${fieldType.show}'")
              case _ =>
                if (fieldType.typeSymbol.isTypeParam) {
                  fail(s"Field type ${fieldType.show} isTypeParam, probaly error in substitution\n" +
                    s"tpe: ${tpe.show} ($tpe), originFieldType: ${originFieldType.show} (${originFieldType}), " +
                    s"typeParams: $typeParams")
                } else if (defaultValue.isDefined && !(defaultValue.get.tpe <:< fieldType)) {
                  fail("Polymorphic expression cannot be instantiated to expected type: default value for " +
                    s"field $symbol of class ${tpe.show} have type ${defaultValue.get.tpe.show} but field type " +
                    s"is ${fieldType.show}")
                }
            fieldInfos.addOne(FieldInfo(symbol, mappedName, getterOrField, defaultValue, fieldType, isTransient, isStringified, nonTransientFieldIndex))
            if (!isTransient) nonTransientFieldIndex += 1
          }
          fieldInfos.toIndexedSeq
        }

        def isTypeParamsList(symbols: List[Symbol]): Boolean = symbols.exists(_.isTypeParam)

        ClassInfo(tpe, primaryConstructor, primaryConstructor.paramSymss match {
          case typeParams :: Nil if isTypeParamsList(typeParams) => createFieldInfos(Nil, typeParams)
          case params :: Nil => createFieldInfos(params, Nil)
          case typeParams :: params :: Nil if isTypeParamsList(typeParams) => createFieldInfos(params, typeParams)
          case _ => fail(s"'${tpe.show}' hasn't a primary constructor with one parameter list. " +
              "Please consider using a custom implicitly accessible codec for this type.\n" +
              s"primaryConstructor.paramSymss = ${primaryConstructor.paramSymss}\n")
        })
      })

      def genReadKey[T: Type](types: List[TypeRepr], in: Expr[JsonReader])(using Quotes): Expr[T] = {
        val tpe = types.head
        val implKeyCodec = findImplicitKeyCodec(types)
        if (!implKeyCodec.isEmpty)  '{ ${implKeyCodec.get}.decodeKey($in) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Boolean] || tpe =:= TypeRepr.of[java.lang.Boolean])  '{ $in.readKeyAsBoolean() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Byte] || tpe =:= TypeRepr.of[java.lang.Byte])  '{ $in.readKeyAsByte() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Char] || tpe =:= TypeRepr.of[java.lang.Character]) '{ $in.readKeyAsChar() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Short] || tpe =:= TypeRepr.of[java.lang.Short]) '{ $in.readKeyAsShort() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Int] || tpe =:= TypeRepr.of[java.lang.Integer]) '{ $in.readKeyAsInt() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Long] || tpe =:= TypeRepr.of[java.lang.Long]) '{ $in.readKeyAsLong() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Float] || tpe =:= TypeRepr.of[java.lang.Float]) '{ $in.readKeyAsFloat() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Double] || tpe =:= TypeRepr.of[java.lang.Double]) '{ $in.readKeyAsDouble() }.asExprOf[T]
        else if (isValueClass(tpe)) {
          val vtpe = valueClassValueType(tpe)
          vtpe.asType match
            case '[vt] =>
              getClassInfo(tpe).genNew(List(genReadKey[vt](vtpe :: types, in).asTerm)).asExprOf[T]
        } else if (tpe =:= TypeRepr.of[String]) '{ $in.readKeyAsString() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[BigInt]) '{ $in.readKeyAsBigInt(${Expr(cfg.bigIntDigitsLimit)}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[BigDecimal]) {
          val mc = withMathContextFor(cfg.bigDecimalPrecision)
          '{ $in.readKeyAsBigDecimal($mc, ${Expr(cfg.bigDecimalScaleLimit)}, ${Expr(cfg.bigDecimalDigitsLimit)}) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.util.UUID]) '{ $in.readKeyAsUUID() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Duration]) '{ $in.readKeyAsDuration() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Instant]) '{ $in.readKeyAsInstant() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[LocalDate]) '{ $in.readKeyAsLocalDate() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[LocalDateTime]) '{ $in.readKeyAsLocalDateTime() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[LocalTime]) '{ $in.readKeyAsLocalTime() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[MonthDay]) '{ $in.readKeyAsMonthDay() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[OffsetDateTime]) '{ $in.readKeyAsOffsetDateTime() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[OffsetTime]) '{ $in.readKeyAsOffsetTime() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Period]) '{ $in.readKeyAsPeriod() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Year]) '{ $in.readKeyAsYear() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[YearMonth]) '{ $in.readKeyAsYearMonth() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[ZonedDateTime]) '{ $in.readKeyAsZonedDateTime() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[ZoneId]) '{ $in.readKeyAsZoneId() }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[ZoneOffset]) '{ $in.readKeyAsZoneOffset() }.asExprOf[T]
        else if (tpe <:< TypeRepr.of[Enumeration#Value]) {
          if (cfg.useScalaEnumValueId) {
            val ec =
              if (MacroUtils.isNative) withScala2EnumerationSingleThreadCacheFor[Int, T & Enumeration#Value](tpe)
              else withScala2EnumerationConcurrentCacheFor[Int, T & Enumeration#Value](tpe)
            '{
              val i = $in.readKeyAsInt()
              var x = $ec.get(i)
              if (x eq null) {
                x = ${findScala2EnumerationById[T & Enumeration#Value](tpe, 'i)}.getOrElse($in.enumValueError(i.toString))
                $ec.put(i, x)
              }
              x
            }.asExprOf[T]
          } else {
            val ec =
              if (MacroUtils.isNative) withScala2EnumerationSingleThreadCacheFor[String, T & Enumeration#Value](tpe)
              else withScala2EnumerationConcurrentCacheFor[String, T & Enumeration#Value](tpe)
            '{
              val s = $in.readKeyAsString()
              var x = $ec.get(s)
              if (x eq null) {
                x = ${findScala2EnumerationByName[T & Enumeration#Value](tpe, 's)}.getOrElse($in.enumValueError(s.length))
                $ec.put(s, x)
              }
              x
            }.asExprOf[T]
          }
        } else if (isJavaEnum(tpe)) {
          '{
            val l = $in.readKeyAsCharBuf()
            ${genReadJavaEnumValue(javaEnumValues(tpe), '{ $in.enumValueError(l) }, in, 'l)}
          }.asExprOf[T]
        } else if (isConstType(tpe)) {
          tpe match
            case ConstantType(StringConstant(v)) =>
              '{ if ($in.readKeyAsString() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case ConstantType(BooleanConstant(v)) =>
              '{ if ($in.readKeyAsBoolean() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case ConstantType(ByteConstant(v)) =>
              '{ if ($in.readKeyAsByte() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case ConstantType(CharConstant(v)) =>
              '{ if ($in.readKeyAsChar() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case ConstantType(ShortConstant(v)) =>
              '{ if ($in.readKeyAsShort() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case ConstantType(IntConstant(v)) =>
              '{ if ($in.readKeyAsInt() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case ConstantType(LongConstant(v)) =>
              '{ if ($in.readKeyAsLong() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case ConstantType(FloatConstant(v)) =>
              '{ if ($in.readKeyAsFloat() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case ConstantType(DoubleConstant(v)) =>
              '{ if ($in.readKeyAsDouble() != ${Expr(v)}) $in.decodeError(${Expr(s"expected key: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
            case _ => cannotFindKeyCodecError(tpe)
        } else cannotFindKeyCodecError(tpe)
      }

      sealed trait UpdateOp // FIXME: type parameter here trigger dotty bug: see https://github.com/lampepfl/dotty/issues/14123

      case class Assignment(value: Term) extends UpdateOp

      case class Update(operation: Expr[Unit]) extends UpdateOp

      case class ConditionalAssignmentAndUpdate(cond: Expr[Boolean], assignment: Term, update: Expr[Unit]) extends UpdateOp

      def genReadArray[B: Type, C: Type](newBuilder: Expr[B], readVal: Quotes ?=> (Expr[B], Expr[Int]) => UpdateOp,
                                         default: Expr[C], result: Quotes ?=> (Expr[B], Expr[Int]) => Expr[C],
                                         in: Expr[JsonReader])(using Quotes): Expr[C] = '{
        if ($in.isNextToken('[')) {
          if ($in.isNextToken(']')) $default
          else {
            $in.rollbackToken()
            var x = $newBuilder
            var i = 0
            while ({
              ${readVal('x, 'i) match
                case Assignment(value) => '{ x = ${value.asExprOf[B]} }
                case Update(operation) => operation
                case ConditionalAssignmentAndUpdate(cond, value, operation) => '{
                  if ($cond) x = ${value.asExprOf[B]}
                  $operation
                }
              }
              i += 1
              $in.isNextToken(',')
            }) ()
            if ($in.isCurrentToken(']')) ${result('x, 'i)}
            else $in.arrayEndOrCommaError()
          }
        } else $in.readNullOrTokenError($default, '[')
      }

      def genReadSet[B: Type, C: Type](newBuilder: Expr[B], readVal: Quotes ?=> Expr[B] => UpdateOp, default: Expr[C],
                                       result: Quotes ?=> Expr[B] => Expr[C], in: Expr[JsonReader])(using Quotes): Expr[C] = '{
        if ($in.isNextToken('[')) {
          if ($in.isNextToken(']')) $default
          else {
            $in.rollbackToken()
            var x = $newBuilder
            var i = 0
            while ({
              ${readVal('x) match
                case Assignment(value) => '{ x = ${value.asExprOf[B]} }
                case Update(operation) => operation
                case ConditionalAssignmentAndUpdate(cond, value, operation) => '{
                  if ($cond) x = ${value.asExprOf[B]}
                  $operation
                }
              }
              i += 1
              if (i > ${Expr(cfg.setMaxInsertNumber)}) $in.decodeError("too many set inserts")
              $in.isNextToken(',')
            }) ()
            if ($in.isCurrentToken(']')) ${result('x)}
            else $in.arrayEndOrCommaError()
          }
        } else $in.readNullOrTokenError($default, '[')
      }

      def genReadMap[B: Type, C: Type](newBuilder: Expr[B], readKV: Quotes ?=> Expr[B]=> UpdateOp,
                                       result: Quotes ?=> Expr[B]=>Expr[C], in: Expr[JsonReader],
                                       default: Expr[C])(using Quotes): Expr[C] = '{
        if ($in.isNextToken('{')) {
          if ($in.isNextToken('}')) $default
          else {
            $in.rollbackToken()
            var x = $newBuilder
            var i = 0
            while ({
              ${readKV('x) match
                case Assignment(value) => '{ x = ${value.asExprOf[B]} }
                case Update(op) => op
                case ConditionalAssignmentAndUpdate(cond, value, update) => '{
                  if ($cond) x = ${value.asExprOf[B]}
                  $update
                }
              }
              i += 1
              if (i > ${Expr(cfg.mapMaxInsertNumber)}) $in.decodeError("too many map inserts")
              $in.isNextToken(',')
            }) ()
            if ($in.isCurrentToken('}')) ${result('x)}
            else $in.objectEndOrCommaError()
          }
        } else $in.readNullOrTokenError($default, '{')
      }

      def genReadMapAsArray[B: Type, C: Type](newBuilder: Expr[B], readKV: Quotes ?=> Expr[B] => UpdateOp,
                                              result: Quotes ?=> Expr[B] => Expr[C], in: Expr[JsonReader],
                                              default: Expr[C])(using Quotes): Expr[C] = '{
        if ($in.isNextToken('[')) {
          if ($in.isNextToken(']')) $default
          else {
            $in.rollbackToken()
            var b = $newBuilder
            var i = 0
            while ({
              if ($in.isNextToken('[')) {
                ${readKV('b) match
                  case Assignment(value) => '{ b = ${value.asExprOf[B]} }
                  case Update(op) => op
                  case ConditionalAssignmentAndUpdate(cond, value, update) =>
                    '{
                      if ($cond) b = ${value.asExprOf[B]}
                      $update
                    }
                }
                i += 1
                if (i > ${Expr(cfg.mapMaxInsertNumber)}) $in.decodeError("too many map inserts")
                if (!$in.isNextToken(']')) $in.arrayEndError()
              } else $in.readNullOrTokenError($default, '[')
              $in.isNextToken(',')
            }) ()
            if ($in.isCurrentToken(']')) ${result('b)}
            else $in.objectEndOrCommaError()
          }
        } else $in.readNullOrTokenError($default, '[')
      }

      @tailrec
      def genWriteKey[T: Type](x: Expr[T], types: List[TypeRepr], out: Expr[JsonWriter])(using Quotes): Expr[Unit] = {
        val tpe = types.head
        val implKeyCodec = findImplicitKeyCodec(types)
        if (!implKeyCodec.isEmpty) '{ ${implKeyCodec.get.asExprOf[JsonKeyCodec[T]]}.encodeKey($x, $out) }
        else if (tpe =:= TypeRepr.of[Boolean]) '{ $out.writeKey(${x.asExprOf[Boolean]}) }
        else if (tpe =:= TypeRepr.of[java.lang.Boolean]) '{ $out.writeKey(${x.asExprOf[java.lang.Boolean]}) }
        else if (tpe =:= TypeRepr.of[Byte]) '{ $out.writeKey(${x.asExprOf[Byte]}) }
        else if (tpe =:= TypeRepr.of[java.lang.Byte]) '{ $out.writeKey(${x.asExprOf[java.lang.Byte]}) }
        else if (tpe =:= TypeRepr.of[Char]) '{ $out.writeKey(${x.asExprOf[Char]}) }
        else if (tpe =:= TypeRepr.of[java.lang.Character]) '{ $out.writeKey(${x.asExprOf[java.lang.Character]}) }
        else if (tpe =:= TypeRepr.of[Short]) '{ $out.writeKey(${x.asExprOf[Short]}) }
        else if (tpe =:= TypeRepr.of[java.lang.Short]) '{ $out.writeKey(${x.asExprOf[java.lang.Short]}) }
        else if (tpe =:= TypeRepr.of[Int]) '{ $out.writeKey(${x.asExprOf[Int]}) }
        else if (tpe =:= TypeRepr.of[Integer]) '{ $out.writeKey(${x.asExprOf[java.lang.Integer]}) }
        else if (tpe =:= TypeRepr.of[Long]) '{ $out.writeKey(${x.asExprOf[Long]}) }
        else if (tpe =:= TypeRepr.of[java.lang.Long]) '{ $out.writeKey(${x.asExprOf[java.lang.Long]}) }
        else if (tpe =:= TypeRepr.of[Float]) '{ $out.writeKey(${x.asExprOf[Float]}) }
        else if (tpe =:= TypeRepr.of[java.lang.Float]) '{ $out.writeKey(${x.asExprOf[java.lang.Float]}) }
        else if (tpe =:= TypeRepr.of[Double]) '{ $out.writeKey(${x.asExprOf[Double]}) }
        else if (tpe =:= TypeRepr.of[java.lang.Double]) '{ $out.writeKey(${x.asExprOf[java.lang.Double]}) }
        else if (tpe =:= TypeRepr.of[String]) '{ $out.writeKey(${x.asExprOf[String]}) }
        else if (tpe =:= TypeRepr.of[BigInt]) '{ $out.writeKey(${x.asExprOf[BigInt]}) }
        else if (tpe =:= TypeRepr.of[BigDecimal]) '{ $out.writeKey(${x.asExprOf[BigDecimal]}) }
        else if (tpe =:= TypeRepr.of[java.util.UUID]) '{ $out.writeKey(${x.asExprOf[java.util.UUID]}) }
        else if (tpe =:= TypeRepr.of[Duration]) '{ $out.writeKey(${x.asExprOf[Duration]}) }
        else if (tpe =:= TypeRepr.of[Instant]) '{ $out.writeKey(${x.asExprOf[Instant]}) }
        else if (tpe =:= TypeRepr.of[LocalDate]) '{ $out.writeKey(${x.asExprOf[LocalDate]}) }
        else if (tpe =:= TypeRepr.of[LocalDateTime]) '{ $out.writeKey(${x.asExprOf[LocalDateTime]}) }
        else if (tpe =:= TypeRepr.of[LocalTime]) '{ $out.writeKey(${x.asExprOf[LocalTime]}) }
        else if (tpe =:= TypeRepr.of[MonthDay]) '{ $out.writeKey(${x.asExprOf[MonthDay]}) }
        else if (tpe =:= TypeRepr.of[OffsetDateTime]) '{ $out.writeKey(${x.asExprOf[OffsetDateTime]}) }
        else if (tpe =:= TypeRepr.of[OffsetTime]) '{ $out.writeKey(${x.asExprOf[OffsetTime]}) }
        else if (tpe =:= TypeRepr.of[Period]) '{ $out.writeKey(${x.asExprOf[Period]}) }
        else if (tpe =:= TypeRepr.of[Year]) '{ $out.writeKey(${x.asExprOf[Year]}) }
        else if (tpe =:= TypeRepr.of[YearMonth]) '{ $out.writeKey(${x.asExprOf[YearMonth]}) }
        else if (tpe =:= TypeRepr.of[ZonedDateTime]) '{ $out.writeKey(${x.asExprOf[ZonedDateTime]}) }
        else if (tpe =:= TypeRepr.of[ZoneId]) '{ $out.writeKey(${x.asExprOf[ZoneId]}) }
        else if (tpe =:= TypeRepr.of[ZoneOffset]) '{ $out.writeKey(${x.asExprOf[ZoneOffset]}) }
        else if (isValueClass(tpe)) {
          val vtpe = valueClassValueType(tpe)
          vtpe.asType match
            case '[vt] => genWriteKey(Select.unique(x.asTerm, valueClassValue(tpe).name).asExprOf[vt], vtpe :: types, out)
        } else if (tpe <:< TypeRepr.of[Enumeration#Value]) {
          if (cfg.useScalaEnumValueId) '{ $out.writeKey(${x.asExprOf[Enumeration#Value]}.id) }
          else '{ $out.writeKey($x.toString) }
        } else if (isJavaEnum(tpe)) {
          val es = javaEnumValues(tpe)
          val encodingRequired = es.exists(e => isEncodingRequired(e.name))
          if (es.exists(_.transformed)) {
            val cases = es.map(e => CaseDef(Ref(e.value), None, Literal(StringConstant(e.name)))) :+
              CaseDef(Wildcard(), None, '{ $out.encodeError("illegal enum value: " + $x) }.asTerm)
            val matchExpr = Match(x.asTerm, cases.toList).asExprOf[String]
            if (encodingRequired) '{ $out.writeKey($matchExpr) }
            else '{ $out.writeNonEscapedAsciiKey($matchExpr) }
          } else {
            val nameExpr = Apply(Select.unique(x.asTerm, "name"), List()).asExprOf[String]
            if (encodingRequired) '{ $out.writeKey($nameExpr) }
            else '{ $out.writeNonEscapedAsciiKey($nameExpr) }
          }
        } else if (isConstType(tpe)) {
          tpe match
            case ConstantType(StringConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case ConstantType(BooleanConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case ConstantType(ByteConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case ConstantType(CharConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case ConstantType(ShortConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case ConstantType(IntConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case ConstantType(LongConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case ConstantType(FloatConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case ConstantType(DoubleConstant(v)) => '{ $out.writeKey(${Expr(v)}) }
            case _ => cannotFindKeyCodecError(tpe)
        } else cannotFindKeyCodecError(tpe)
      }

      def genWriteConstantKey(name: String, out: Expr[JsonWriter])(using Quotes): Expr[Unit] =
        if (isEncodingRequired(name)) '{ $out.writeKey(${Expr(name)}) }
        else '{ $out.writeNonEscapedAsciiKey(${Expr(name)}) }

      def genWriteConstantVal(value: String, out: Expr[JsonWriter])(using Quotes): Expr[Unit] =
        if (isEncodingRequired(value)) '{ $out.writeVal(${Expr(value)}) }
        else '{ $out.writeNonEscapedAsciiVal(${Expr(value)}) }

      def genWriteArray[T: Type](x: Expr[Iterable[T]], writeVal: Quotes ?=> (Expr[JsonWriter], Expr[T]) => Expr[Unit],
                                 out: Expr[JsonWriter])(using Quotes): Expr[Unit] = '{
        $out.writeArrayStart()
        $x.foreach(x => ${writeVal(out, 'x)})
        $out.writeArrayEnd()
      }

      def genWriteMapScala213[K: Type, V: Type](x: Expr[collection.Map[K, V]],
                                                writeKey: Quotes ?=> (Expr[JsonWriter], Expr[K]) => Expr[Unit],
                                                writeVal: Quotes ?=> (Expr[JsonWriter], Expr[V]) => Expr[Unit],
                                                out: Expr[JsonWriter])(using Quotes): Expr[Unit] = '{
        $out.writeObjectStart()
        $x.foreachEntry { (k, v) =>
          ${writeKey(out, 'k)}
          ${writeVal(out, 'v)}
        }
        $out.writeObjectEnd()
      }

      def genWriteMapAsArrayScala213[K: Type, V: Type](x: Expr[collection.Map[K, V]],
                                                       writeKey: Quotes ?=> (Expr[JsonWriter], Expr[K]) => Expr[Unit],
                                                       writeVal: Quotes ?=> (Expr[JsonWriter], Expr[V]) => Expr[Unit],
                                                       out: Expr[JsonWriter])(using Quotes): Expr[Unit] = '{
        $out.writeArrayStart()
        $x.foreachEntry { (k, v) =>
          $out.writeArrayStart()
          ${writeKey(out, 'k)}
          ${writeVal(out, 'v)}
          $out.writeArrayEnd()
        }
        $out.writeArrayEnd()
      }

      def cannotFindKeyCodecError(tpe: TypeRepr): Nothing =
        fail(s"No implicit '${TypeRepr.of[JsonKeyCodec[_]].show}' defined for '${tpe.show}'.")

      def cannotFindValueCodecError(tpe: TypeRepr): Nothing =
        fail(if (tpe.typeSymbol.flags.is(Flags.Abstract) || tpe.typeSymbol.flags.is(Flags.Trait)) {
          "Only sealed traits or abstract classes are supported as an ADT base. " +
            s"Please consider sealing the '${tpe.show}' or provide a custom implicitly accessible codec for it."
        } else s"No implicit '${TypeRepr.of[JsonValueCodec[_]].show}' defined for '${tpe.show}'.")

      def namedValueOpt(namedAnnotation: Option[Term], tpe: TypeRepr): Option[String] = namedAnnotation.map {
        case Apply(_, List(param)) => CompileTimeEval.evalExpr(param.asExprOf[String]).asTerm match // TODO: write testcase
          case Literal(StringConstant(s)) => s
          case _ => fail(s"Cannot evaluate a parameter of the '@named' annotation in type '${tpe.show}': $param.")
        case a => fail(s"Invalid named annotation ${a.show}")
      }

      def unexpectedFieldHandler(in: Expr[JsonReader], l: Expr[Int])(using Quotes): Expr[Unit] =
        if (cfg.skipUnexpectedFields) '{ $in.skip() }
        else '{ $in.unexpectedKeyError($l) }

      def discriminatorValue(tpe: TypeRepr): String =
        val named = tpe.typeSymbol.annotations.filter(_.tpe =:= TypeRepr.of[named])
        if (named.size > 1) fail(s"Duplicated '${TypeRepr.of[named].show}' defined for '${tpe.show}'.")
        if (named.size > 0) namedValueOpt(named.headOption, tpe).get
        else cfg.adtLeafClassNameMapper({
          if (tpe.typeSymbol.flags.is(Flags.Enum)) {
            tpe match
              case TermRef(_, name) => name
              case TypeRef(_, name) => name // ADT
              case AppliedType(base, _) => base.typeSymbol.fullName
              case _ => fail(s"Unsupported enum type: '${tpe.show}', tree=$tpe")
          } else if (tpe.typeSymbol.flags.is(Flags.Module)) tpe.termSymbol.fullName // FIXME: This check fixes names. Should it be reported as Dotty bug?
          else tpe.typeSymbol.fullName
        }).getOrElse(fail(s"Discriminator is not defined for ${tpe.show}"))

      def checkFieldNameCollisions(tpe: TypeRepr, names: Seq[String]): Unit =
        val collisions = duplicated(names)
        if (collisions.nonEmpty) {
          val formattedCollisions = collisions.mkString("'", "', '", "'")
          fail(s"Duplicated JSON key(s) defined for '${tpe.show}': $formattedCollisions. Keys are derived from " +
            s"field names of the class that are mapped by the '${TypeRepr.of[CodecMakerConfig].show}.fieldNameMapper' " +
            s"function or can be overridden by '${TypeRepr.of[named].show}' annotation(s). Result keys should be " +
            s"unique and should not match with a key for the discriminator field that is specified by the " +
            s"'${TypeRepr.of[CodecMakerConfig].show}.discriminatorFieldName' option.")
        }

      def checkDiscriminatorValueCollisions(tpe: TypeRepr, names: Seq[String]): Unit =
        val collisions = duplicated(names)
        if (collisions.nonEmpty) {
          val formattedCollisions = collisions.mkString("'", "', '", "'")
          fail(s"Duplicated discriminator defined for ADT base '${tpe.show}': $formattedCollisions. Values for " +
            s"leaf classes of ADT that are returned by the '${Type.show[CodecMakerConfig]}.adtLeafClassNameMapper' " +
            s"function should be unique. Names: $names")
        }

      val nullValues = new mutable.LinkedHashMap[TypeRepr, ValDef]

      def withNullValueFor[T: Type](tpe: TypeRepr)(f: => Expr[T]): Expr[T] = Ref(nullValues.getOrElseUpdate(tpe, {
        val sym = symbol("c" + nullValues.size, tpe)
        ValDef(sym, Some(f.asTerm.changeOwner(sym)))
      }).symbol).asExprOf[T]

      val fieldIndexAccessors = new mutable.LinkedHashMap[TypeRepr, DefDef]

      def withFieldsByIndexFor(tpe: TypeRepr)(f: => Seq[String]): Term =
        Ref(fieldIndexAccessors.getOrElseUpdate(tpe, { // [Int => String], we don't want eta-expand without reason, so let this will be just index
          val mt = MethodType(List("i"))(_ => List(TypeRepr.of[Int]), _ => TypeRepr.of[String])
          val sym = Symbol.newMethod(Symbol.spliceOwner, "f" + fieldIndexAccessors.size, mt)
          DefDef(sym, params => {
            val List(List(param)) = params
            val paramTerm = param match
              case term: Term => term
              case _ => fail(s"Expected that $param is term")
            val cases = f.zipWithIndex
              .map { case (n, i) => CaseDef(Literal(IntConstant(i)), None, Literal(StringConstant(n))) }
            Some(Match(paramTerm, cases.toList).changeOwner(sym))
          })
        }).symbol)

      val equalsMethods = new mutable.LinkedHashMap[TypeRepr, DefDef]

      def withEqualsFor[T: Type](tpe: TypeRepr, arg1: Expr[T], arg2: Expr[T])
                                (f: (Expr[T], Expr[T]) => Expr[Boolean]): Expr[Boolean] =
        Apply(Ref(equalsMethods.getOrElseUpdate(tpe, {
          val mt = MethodType(List("x1", "x2"))(_ => List(tpe, tpe), _ => TypeRepr.of[Boolean])
          val sym = Symbol.newMethod(Symbol.spliceOwner, "q" + equalsMethods.size, mt)
          DefDef(sym, params => {
            val List(List(x1, x2)) = params
            Some(f(x1.asExprOf[T], x2.asExprOf[T]).asTerm.changeOwner(sym))
          })
        }).symbol), List(arg1.asTerm, arg2.asTerm)).asExprOf[Boolean]

      def genArrayEquals[T: Type](tpe: TypeRepr, x1t: Expr[T], x2t: Expr[T]): Expr[Boolean] = {
        val tpe1 = typeArg1(tpe)
        if (tpe1 <:< TypeRepr.of[Array[_]]) {
          tpe1.asType match
            case '[t1] =>
              val x1 = x1t.asExprOf[Array[t1]]
              val x2 = x2t.asExprOf[Array[t1]]

              def arrEquals(i: Expr[Int])(using Quotes): Expr[Boolean] =
                withEqualsFor(tpe1, '{ $x1($i) }, '{ $x2($i) })((x1, x2) => genArrayEquals(tpe1, x1, x2))

              '{
                ($x1 eq $x2) || (($x1 ne null) && ($x2 ne null) && {
                  val l1 = $x1.length
                  val l2 = $x2.length
                  (l1 == l2) && {
                    var i = 0
                    while (i < l1 && ${arrEquals('i)}) i += 1
                    i == l1
                  }
                })
              }
        } else if (tpe1 <:< TypeRepr.of[Boolean]) {
          '{ java.util.Arrays.equals(${x1t.asExprOf[Array[Boolean]]}, ${x2t.asExprOf[Array[Boolean]]}) }
        } else if (tpe1 <:< TypeRepr.of[Byte]) {
          '{ java.util.Arrays.equals(${x1t.asExprOf[Array[Byte]]}, ${x2t.asExprOf[Array[Byte]]}) }
        } else if (tpe1 <:< TypeRepr.of[AnyRef]) {
          '{ java.util.Arrays.equals(${x1t.asExprOf[Array[AnyRef]]}, ${x2t.asExprOf[Array[AnyRef]]}) }
        } else if (tpe1 <:< TypeRepr.of[Short]) {
          '{ java.util.Arrays.equals(${x1t.asExprOf[Array[Short]]}, ${x2t.asExprOf[Array[Short]]}) }
        } else if (tpe1 <:< TypeRepr.of[Int]) {
          '{ java.util.Arrays.equals(${x1t.asExprOf[Array[Int]]}, ${x2t.asExprOf[Array[Int]]}) }
        } else if (tpe1 <:< TypeRepr.of[Float]) {
          '{ java.util.Arrays.equals(${x1t.asExprOf[Array[Float]]}, ${x2t.asExprOf[Array[Float]]}) }
        } else if (tpe1 <:< TypeRepr.of[Double]) {
          '{ java.util.Arrays.equals(${x1t.asExprOf[Array[Double]]}, ${x2t.asExprOf[Array[Double]]}) }
        } else if (tpe1 <:< TypeRepr.of[Char]) {
          '{ java.util.Arrays.equals(${x1t.asExprOf[Array[Char]]}, ${x2t.asExprOf[Array[Char]]}) }
        } else fail(s"Can't generate array of type ${tpe1.show}")
      }

      case class DecoderMethodKey(tpe: TypeRepr, isStringified: Boolean, useDiscriminator: Boolean)

      val decodeMethodDefs = new mutable.LinkedHashMap[DecoderMethodKey, DefDef]
      val decodeMethodSyms = new mutable.LinkedHashMap[DecoderMethodKey, Symbol]

      def withDecoderFor[T: Type](methodKey: DecoderMethodKey, arg: Expr[T], in: Expr[JsonReader])
                                 (f: (Expr[JsonReader], Expr[T]) => Expr[T])(using Quotes): Expr[T] =
        Apply(Ref(decodeMethodSyms.get(methodKey).getOrElse {
          val mt = MethodType(List("in", "defaultValue"))(_ => List(TypeRepr.of[JsonReader], methodKey.tpe), _ => TypeRepr.of[T])
          val sym = Symbol.newMethod(Symbol.spliceOwner, "d" + decodeMethodSyms.size, mt)
          decodeMethodSyms.update(methodKey, sym)
          decodeMethodDefs.update(methodKey, {
            DefDef(sym, params => {
              val List(List(in, default)) = params
              val defaultExpr = default.asExprOf[T]
              val res = f(in.asExprOf[JsonReader], defaultExpr).asTerm.asInstanceOf[quotes.reflect.Term]
              val sym1 = sym.asInstanceOf[quotes.reflect.Symbol]
              val res1 = LowLevelQuoteUtil.deepChangeOwner(res, sym1, false /* TODO: rewise the flag */).asInstanceOf[Term]
              if (traceFlag) LowLevelQuoteUtil.checkOwner(res1.asInstanceOf[quotes.reflect.Term], sym1)
              Some(res1)
            })
          })
          sym
        }), List(in.asTerm, arg.asTerm)).asExprOf[T]

      case class WriteDiscriminator(fieldName: String, fieldValue: String) {
        def write(out: Expr[JsonWriter]): Expr[Unit] = '{
          ${genWriteConstantKey(fieldName, out)}
          ${genWriteConstantVal(fieldValue, out)}
        }
      }

      case class EncoderMethodKey(tpe: TypeRepr, isStringified: Boolean, discriminatorKeyValue: Option[(String, String)])

      val encodeMethodDefs = new mutable.LinkedHashMap[EncoderMethodKey, DefDef]
      val encodeMethodSyms = new mutable.LinkedHashMap[EncoderMethodKey, Symbol]

      def withEncoderFor[T: Type](methodKey: EncoderMethodKey, arg: Expr[T], out: Expr[JsonWriter])
                                 (f: (Expr[JsonWriter], Expr[T])=> Expr[Unit]): Expr[Unit] =
        Apply(Ref(encodeMethodSyms.get(methodKey).getOrElse {
          val mt = MethodType(List("x", "out"))(_ => List(TypeRepr.of[T], TypeRepr.of[JsonWriter]), _ => TypeRepr.of[Unit])
          val sym = Symbol.newMethod(Symbol.spliceOwner, "e" + encodeMethodSyms.size, mt)
          encodeMethodSyms.update(methodKey, sym)
          encodeMethodDefs.update(methodKey, {
            DefDef(sym, params => {
              val List(List(x, out)) = params
              Some(f(out.asExprOf[JsonWriter], x.asExprOf[T]).asTerm.changeOwner(sym))
            })
          })
          sym
        }), List(arg.asTerm, out.asTerm)).asExprOf[Unit]

      def genNullValue[T: Type](types: List[TypeRepr])(using Quotes): Expr[T] =
        val tpe = types.head
        val implCodec = findImplicitValueCodec(types)
        if (!implCodec.isEmpty) '{ ${implCodec.get}.nullValue }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Boolean]) Literal(BooleanConstant(false)).asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.lang.Boolean]) '{ java.lang.Boolean.valueOf(false) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Byte]) Literal(ByteConstant(0)).asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.lang.Byte]) '{ java.lang.Byte.valueOf(0: Byte) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Char]) '{ '\u0000' }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.lang.Character]) '{ java.lang.Character.valueOf('\u0000') }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Short]) Literal(ShortConstant(0)).asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.lang.Short]) '{ java.lang.Short.valueOf(0: Short) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Int]) Literal(IntConstant(0)).asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.lang.Integer]) '{ java.lang.Integer.valueOf(0) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Long]) Literal(LongConstant(0)).asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.lang.Long]) '{ java.lang.Long.valueOf(0L) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Float]) Literal(FloatConstant(0f)).asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.lang.Float]) '{ java.lang.Float.valueOf(0f) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Double]) Literal(DoubleConstant(0d)).asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.lang.Double]) '{ java.lang.Double.valueOf(0d) }.asExprOf[T]
        else if (isOption(tpe)) '{ None }.asExprOf[T]
        else if (tpe <:< TypeRepr.of[mutable.BitSet]) '{ new mutable.BitSet }.asExprOf[T]
        else if (tpe <:< TypeRepr.of[immutable.BitSet]) withNullValueFor(tpe)('{ immutable.BitSet.empty }.asExprOf[T])
        else if (tpe <:< TypeRepr.of[collection.BitSet]) withNullValueFor(tpe)('{ collection.BitSet.empty }.asExprOf[T])
        else if (tpe <:< TypeRepr.of[::[_]]) '{ null }.asExprOf[T]
        else if (tpe <:< TypeRepr.of[List[_]] || tpe =:= TypeRepr.of[Seq[_]]) '{ Nil }.asExprOf[T]
        else if (tpe <:< TypeRepr.of[collection.SortedSet[_]]) withNullValueFor(tpe) {
          val tpe1 = typeArg1(tpe)
          Apply(scalaCollectionEmptyNoArgs(tpe, tpe1), List(summonOrdering(tpe1))).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[immutable.IntMap[_]] || tpe <:< TypeRepr.of[immutable.LongMap[_]] ||
            tpe <:< TypeRepr.of[mutable.LongMap[_]] || tpe <:< TypeRepr.of[immutable.Seq[_]] ||
            tpe <:< TypeRepr.of[Set[_]]) withNullValueFor(tpe) {
          scalaCollectionEmptyNoArgs(tpe, typeArg1(tpe)).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[immutable.SortedMap[_, _]]) withNullValueFor(tpe) {
          val tpe1 = typeArg1(tpe)
          Apply(scalaMapEmptyNoArgs(tpe, tpe1, typeArg2(tpe)), List(summonOrdering(tpe1))).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[immutable.Map[_, _]]) withNullValueFor(tpe) {
          scalaMapEmptyNoArgs(tpe, typeArg1(tpe), typeArg2(tpe)).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[collection.Map[_, _]]) {
          scalaMapEmptyNoArgs(tpe, typeArg1(tpe), typeArg2(tpe)).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[mutable.ArraySeq[_]] || tpe <:< TypeRepr.of[mutable.UnrolledBuffer[_]]) {
          val tpe1 = typeArg1(tpe)
          Apply(scalaCollectionEmptyNoArgs(tpe, tpe1), List(summonClassTag(tpe1))).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[Iterable[_]]) scalaCollectionEmptyNoArgs(tpe, typeArg1(tpe)).asExprOf[T]
        else if (tpe <:< TypeRepr.of[Array[_]])
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val t1ClassTag = summonClassTag(tpe1).asExprOf[ClassTag[t1]]
              withNullValueFor(tpe)('{ new Array[t1](0)(using $t1ClassTag) }).asExprOf[T]
        else if (isConstType(tpe)) {
          tpe match
            case ConstantType(StringConstant(v)) => Literal(StringConstant(v)).asExprOf[T]
            case ConstantType(BooleanConstant(v)) => Literal(BooleanConstant(v)).asExprOf[T]
            case ConstantType(ByteConstant(v)) => Literal(ByteConstant(v)).asExprOf[T]
            case ConstantType(CharConstant(v)) => Literal(CharConstant(v)).asExprOf[T]
            case ConstantType(ShortConstant(v)) => Literal(ShortConstant(v)).asExprOf[T]
            case ConstantType(IntConstant(v)) => Literal(IntConstant(v)).asExprOf[T]
            case ConstantType(LongConstant(v)) => Literal(LongConstant(v)).asExprOf[T]
            case ConstantType(FloatConstant(v)) => Literal(FloatConstant(v)).asExprOf[T]
            case ConstantType(DoubleConstant(v)) => Literal(DoubleConstant(v)).asExprOf[T]
            case _ => cannotFindValueCodecError(tpe)
        } else if (tpe.isSingleton) Ref(tpe.termSymbol).asExprOf[T]
        else if (tpe =:= TypeRepr.of[Unit]) '{ () }.asExprOf[T]
        else if (tpe <:< TypeRepr.of[AnyRef]) '{ null }.asExprOf[T]
        else if (tpe <:< TypeRepr.of[AnyVal]) {
          val tpe1 = valueClassValueType(tpe)
          tpe1.asType match
            case '[t1] => getClassInfo(tpe).genNew(List(genNullValue[t1](tpe1 :: types).asTerm)).asExprOf[T]
        } else fail(s"Can't deduce null value for ${tpe.show} tree($tpe)")

      case class ReadDiscriminator(valDef: ValDef) {
        def skip(in: Expr[JsonReader], l: Expr[Int])(using Quotes): Expr[Unit] = '{
          if (${Ref(valDef.symbol).asExprOf[Boolean]}) {
            ${Assign(Ref(valDef.symbol), Literal(BooleanConstant(false))).asExprOf[Unit]}
            $in.skip()
          } else $in.duplicatedKeyError($l)
        }
      }

      def genReadSealedClass[T: Type](types: List[TypeRepr], in: Expr[JsonReader], default: Expr[T],
                                      isStringified: Boolean)(using Quotes): Expr[T] = {
        val tpe = types.head
        if (traceFlag) println(s"genReadSealedClass[${tpe.show}], discriminatorFieldName=${cfg.discriminatorFieldName}")
        val leafClasses = adtLeafClasses(tpe)
        val currentDiscriminator = cfg.discriminatorFieldName
        val discriminatorError =
          cfg.discriminatorFieldName.fold('{ $in.discriminatorError() })(n => '{ $in.discriminatorValueError(${Expr(n)}) })
        def genReadLeafClass[T: Type](subTpe: TypeRepr)(using Quotes): Expr[T] =
          val useDiscriminator = cfg.discriminatorFieldName.isDefined
          if (subTpe =:= tpe) genReadNonAbstractScalaClass(types, useDiscriminator, in, genNullValue[T](types))
          else genReadVal(subTpe :: types, genNullValue[T](subTpe :: types), isStringified, useDiscriminator, in)

        def genReadCollisions[T: Type](subTpes: collection.Seq[TypeRepr], l: Expr[Int])(using Quotes): Expr[T] =
          subTpes.foldRight(discriminatorError.asExprOf[T]) { (subTpe, acc) =>
            subTpe.asType match
              case '[st] => '{
                if ($in.isCharBufEqualsTo($l, ${Expr(discriminatorValue(subTpe))})) ${
                  if (currentDiscriminator.isDefined) '{
                    $in.rollbackToMark()
                    ${genReadLeafClass[st](subTpe)}
                  } else if (isEnumOrModuleValue(subTpe)) Ref(subTpe.termSymbol).asExprOf[st]
                  else genReadLeafClass[st](subTpe)
                } else $acc
              }.asExprOf[T]
          }

        def genReadSubclassesBlock[T: Type](leafClasses: collection.Seq[TypeRepr], l: Expr[Int])(using Quotes): Expr[T] =
          if (leafClasses.size <= 8 && leafClasses.foldLeft(0)(_ + discriminatorValue(_).length) <= 64) {
            genReadCollisions(leafClasses, l)
          } else {
            val hashCode = (t: TypeRepr) => {
              val cs = discriminatorValue(t).toCharArray
              JsonReader.toHashCode(cs, cs.length)
            }
            val cases = groupByOrdered(leafClasses)(hashCode).map { case (hash, ts) =>
              CaseDef(Literal(IntConstant(hash)), None, genReadCollisions(ts, l).asTerm)
            }
            val lastCase = CaseDef(Wildcard(), None, discriminatorError.asTerm)
            val scrutinee = '{ $in.charBufToHashCode($l): @scala.annotation.switch }.asTerm
            Match(scrutinee, (cases :+ lastCase).toList).asExprOf[T]
          }

        checkDiscriminatorValueCollisions(tpe, leafClasses.map(discriminatorValue))

        def genReadJsObjClass(objClasses: Seq[TypeRepr], useCurrentToken: Boolean)(using Quotes): Expr[T] = {
          def checkToken(using Quotes): Expr[Boolean] =
            if (useCurrentToken) '{ $in.isCurrentToken('{') }
            else '{ $in.isNextToken('{') }

          def setMark(using Quotes): Expr[Unit] =
            if (useCurrentToken) '{
              $in.rollbackToken()
              $in.setMark()
            } else '{ $in.setMark() }

          currentDiscriminator match
            case None => '{
              if (${checkToken}) {
                val l = $in.readKeyAsCharBuf()
                val r = ${genReadSubclassesBlock(objClasses, 'l).asExprOf[T]}
                if ($in.isNextToken('}')) r
                else $in.objectEndOrCommaError()
              } else $in.readNullOrError($default, "expected '\"' or '{' or null")
            }
            case Some(discrFieldName) =>
              if (cfg.requireDiscriminatorFirst) '{
                ${setMark}
                if ($in.isNextToken('{')) {
                  if (${Expr(discrFieldName)}.equals($in.readKeyAsString())) {
                    val l = $in.readStringAsCharBuf()
                    ${genReadSubclassesBlock(objClasses, 'l).asExprOf[T]}
                  } else $in.decodeError(${Expr("expected key: \"" + discrFieldName + '"')})
                } else $in.readNullOrTokenError($default, '{')
              } else '{
                ${setMark}
                if ($in.isNextToken('{')) {
                  if ($in.skipToKey(${Expr(discrFieldName)})) {
                    val l = $in.readStringAsCharBuf()
                    ${genReadSubclassesBlock(objClasses, 'l).asExprOf[T]}
                  } else $in.requiredFieldError(${Expr(discrFieldName)})
                } else $in.readNullOrTokenError($default, '{')
              }
        }

        if (currentDiscriminator == None) {
          val (leafModuleClasses, leafCaseClasses) = leafClasses.partition(tpe => isEnumOrModuleValue(tpe))
          if (leafModuleClasses.nonEmpty && leafCaseClasses.nonEmpty) {
            '{
              if ($in.isNextToken('"')) {
                $in.rollbackToken()
                val l = $in.readStringAsCharBuf()
                ${genReadSubclassesBlock(leafModuleClasses, 'l).asExprOf[T]}
              } else ${genReadJsObjClass(leafCaseClasses, true)}
            }.asExprOf[T]
          } else if (leafCaseClasses.nonEmpty) genReadJsObjClass(leafCaseClasses, false)
          else '{
            if ($in.isNextToken('"')) {
              $in.rollbackToken()
              val l = $in.readStringAsCharBuf()
              ${genReadSubclassesBlock(leafModuleClasses, 'l).asExprOf[T]}
            } else $in.readNullOrTokenError($default, '"')
          }.asExprOf[T]
        } else genReadJsObjClass(leafClasses, false)
      }

      def genReadNonAbstractScalaClass[T: Type](types: List[TypeRepr], useDiscriminator: Boolean, in: Expr[JsonReader],
                                                default: Expr[T])(using Quotes): Expr[T] = {
        val tpe = types.head
        if (traceFlag) println(s"genReadNonAbstractScalaClass[${tpe.show}]")
        val classInfo = getClassInfo(tpe)
        checkFieldNameCollisions(tpe, cfg.discriminatorFieldName.fold(Seq.empty[String]) { n =>
          val names = classInfo.nonTransientFields.map(_.mappedName)
          if (!useDiscriminator) names
          else names :+ n
        })
        val required = classInfo.nonTransientFields.collect {
          case f if !(f.symbol.flags.is(Flags.HasDefault) || isOption(f.resolvedTpe) ||
            (isCollection(f.resolvedTpe) && !cfg.requireCollectionFields)) => f.mappedName
        }.toSet
        val paramVarNum = classInfo.nonTransientFields.size
        val lastParamVarIndex = Math.max(0, (paramVarNum - 1) >> 5)
        val lastParamVarBits = -1 >>> -paramVarNum
        val paramVars = (0 to lastParamVarIndex)
          .map(i => ValDef(symbol("p" + i, TypeRepr.of[Int], Flags.Mutable), Some(Literal(IntConstant {
            if (i == lastParamVarIndex) lastParamVarBits
            else -1
          }))))

        def checkAndResetFieldPresenceFlag(name: String, l: Expr[Int])(using Quotes): Expr[Unit] =
          val fi = classInfo.nonTransientFields.find(_.mappedName == name)
            .getOrElse(fail(s"Field $name is not found in fields for $classInfo"))
          val n = Ref(paramVars(fi.nonTransientFieldIndex >> 5).symbol).asExprOf[Int]
          val m = Expr(1 << fi.nonTransientFieldIndex)
          '{
            if (($m & $n) != 0) ${Assign(n.asTerm, '{ $n ^ $m }.asTerm).asExprOf[Unit]}
            else $in.duplicatedKeyError($l)
          }

        val checkReqVars =
          if (required.isEmpty) Nil
          else {
            val nameByIndex = withFieldsByIndexFor(tpe)(classInfo.nonTransientFields.map(_.mappedName))
            val reqMasks = classInfo.nonTransientFields.grouped(32).toSeq.map(_.zipWithIndex.foldLeft(0) {
              case (acc, (f, i)) =>
                if (required(f.mappedName)) acc | (1 << i)
                else acc
            })
            paramVars.zipWithIndex.map { case (nValDef, i) =>
              val n = Ref(nValDef.symbol).asExprOf[Int]
              val m = Expr(reqMasks(i))
              val fieldName =
                if (i == 0) '{ java.lang.Integer.numberOfTrailingZeros($n & $m) }.asTerm
                else '{ java.lang.Integer.numberOfTrailingZeros($n & $m) + ${Expr(i << 5)} }.asTerm
              '{ if (($n & $m) != 0) $in.requiredFieldError(${Apply(nameByIndex, List(fieldName)).asExprOf[String]}) }.asTerm
            }.toList
          }
        val readVars = classInfo.nonTransientFields.map { f =>
          val sym = symbol("_" + f.symbol.name, f.resolvedTpe, Flags.Mutable)
          f.resolvedTpe.asType match
            case '[ft] =>
              ValDef(sym, Some(f.defaultValue.getOrElse(genNullValue[ft](f.resolvedTpe :: types).asTerm.changeOwner(sym))))
        }
        val readVarsMap = classInfo.nonTransientFields.zip(readVars).map { case (field, tmpVar) =>
          (field.symbol.name, tmpVar)
        }.toMap
        val construct = classInfo.genNew(classInfo.allFields.foldLeft(List.newBuilder[Term]) {
          var nonTransientFieldIndex = 0
          (params, fieldInfo) =>
            params.addOne(if (fieldInfo.isTransient) {
              fieldInfo.defaultValue
                .getOrElse(fail(s"Transient field ${fieldInfo.symbol.name} in class ${tpe.show} have no default value"))
            } else {
              val rv = readVars(nonTransientFieldIndex).symbol
              nonTransientFieldIndex += 1
              Ref(rv)
            })
        }.result)
        val readFields = cfg.discriminatorFieldName.fold(classInfo.nonTransientFields) { n =>
          if (!useDiscriminator) classInfo.nonTransientFields
          else classInfo.nonTransientFields :+
            FieldInfo(Symbol.noSymbol, n, FieldInfo.NoField, None, TypeRepr.of[String], isTransient = false,
              isStringified = true, classInfo.nonTransientFields.length)
        }

        def genReadCollisions(fs: collection.Seq[FieldInfo], tmpVars: Map[String, ValDef],
                              discriminator: Option[ReadDiscriminator], l: Expr[Int])(using Quotes): Expr[Unit] =
          fs.foldRight(unexpectedFieldHandler(in, l)) { (f, acc) =>
            val readValue =
              if (discriminator.nonEmpty && cfg.discriminatorFieldName.contains(f.mappedName)) {
                discriminator.get.skip(in, l)
              } else {
                f.resolvedTpe.asType match
                  case '[ft] =>
                    val tmpVar = Ref(tmpVars(f.symbol.name).symbol)
                    Block(List(checkAndResetFieldPresenceFlag(f.mappedName, l).asTerm),
                      Assign(tmpVar, genReadVal(f.resolvedTpe :: types, tmpVar.asExprOf[ft], f.isStringified, false, in).asTerm)).asExprOf[Unit]
              }
            '{
              if ($in.isCharBufEqualsTo($l, ${Expr(f.mappedName)})) $readValue
              else $acc
            }
          }

        val discriminator =
          if (useDiscriminator) {
            cfg.discriminatorFieldName.map { fieldName =>
              val sym = symbol("pd", TypeRepr.of[Boolean], Flags.Mutable)
              ReadDiscriminator(ValDef(sym, Some(Literal(BooleanConstant(true)).changeOwner(sym))))
            }
          } else None
        val optDiscriminatorVar = discriminator.map(_.valDef)

        def readFieldsBlock(l: Expr[Int])(using Quotes): Expr[Unit] = // Using Quotes for w/a, see: https://github.com/lampepfl/dotty/issues/14137
          if (readFields.size <= 8 && readFields.foldLeft(0)(_ + _.mappedName.length) <= 64) {
            genReadCollisions(readFields, readVarsMap, discriminator, l)
          } else {
            val hashCode = (f: FieldInfo) => JsonReader.toHashCode(f.mappedName.toCharArray, f.mappedName.length)
            val cases = groupByOrdered(readFields)(hashCode).map { case (hash, fs) =>
              CaseDef(Literal(IntConstant(hash)), None, genReadCollisions(fs, readVarsMap, discriminator, l).asTerm)
            } :+ CaseDef(Wildcard(), None, unexpectedFieldHandler(in, l).asTerm)
            Match('{ $in.charBufToHashCode($l): @scala.annotation.switch }.asTerm, cases.toList).asExprOf[Unit]
          }

        def blockWithVars(next: Term)(using Quotes): Term =
          Block(readVars.toList ++ paramVars.toList ++ optDiscriminatorVar.toList, next.changeOwner(Symbol.spliceOwner))
            .changeOwner(Symbol.spliceOwner) // All owners should be from top Symbol.spliceOwner because vals are created with this owner

        val readNonEmpty = blockWithVars('{
          if (!$in.isNextToken('}')) {
            $in.rollbackToken()
            var l = -1
            while (l < 0 || $in.isNextToken(',')) {
              l = $in.readKeyAsCharBuf()
              ${readFieldsBlock('l)}
            }
            if (!$in.isCurrentToken('}')) $in.objectEndOrCommaError()
          }
          ${Block(checkReqVars, construct).changeOwner(Symbol.spliceOwner).asExprOf[T]}
        }.asTerm)
        If('{ $in.isNextToken('{') }.asTerm.changeOwner(Symbol.spliceOwner), readNonEmpty,
          '{ $in.readNullOrTokenError($default, '{') }.asTerm.changeOwner(Symbol.spliceOwner)).asExprOf[T]
      }

      def genReadConstType[T: Type](tpe: TypeRepr, isStringified: Boolean, in: Expr[JsonReader])(using Quotes): Expr[T] = tpe match
        case ConstantType(StringConstant(v)) =>
          '{ if ($in.readString(null) != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
        case ConstantType(BooleanConstant(v)) =>
          if (isStringified)
            '{ if ($in.readStringAsBoolean() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
          else
            '{ if ($in.readBoolean() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: $v")}); ${Expr(v)} }.asExprOf[T]
        case ConstantType(ByteConstant(v)) =>
          if (isStringified)
            '{ if ($in.readStringAsByte() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
          else
            '{ if ($in.readByte() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: $v")}); ${Expr(v)} }.asExprOf[T]
        case ConstantType(CharConstant(v)) =>
          '{ if ($in.readChar() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
        case ConstantType(ShortConstant(v)) =>
          if (isStringified)
            '{ if ($in.readStringAsShort() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
          else
            '{ if ($in.readShort() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: $v")}); ${Expr(v)} }.asExprOf[T]
        case ConstantType(IntConstant(v)) =>
          if (isStringified)
            '{ if ($in.readStringAsInt() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
          else
            '{ if ($in.readInt() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: $v")}); ${Expr(v)} }.asExprOf[T]
        case ConstantType(LongConstant(v)) =>
          if (isStringified)
            '{ if ($in.readStringAsLong() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
          else
            '{ if ($in.readLong() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: $v")}); ${Expr(v)} }.asExprOf[T]
        case ConstantType(FloatConstant(v)) =>
          if (isStringified)
            '{ if ($in.readStringAsFloat() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
          else
            '{ if ($in.readFloat() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: $v")}); ${Expr(v)} }.asExprOf[T]
        case ConstantType(DoubleConstant(v)) =>
          if (isStringified)
            '{ if ($in.readStringAsDouble() != ${Expr(v)}) $in.decodeError(${Expr(s"expected value: \"$v\"")}); ${Expr(v)} }.asExprOf[T]
          else
            '{ if ($in.readDouble() != ${Expr(v)}) $in.decodeError(${Expr("expected value: " + v)}); ${Expr(v)} }.asExprOf[T]
        case _ => cannotFindValueCodecError(tpe)

      def genReadValForGrowable[G <: Growable[V]: Type, V: Type](types: List[TypeRepr], isStringified: Boolean,
                                                                 x: Expr[G], in: Expr[JsonReader])(using Quotes): Expr[Unit] =
          '{ $x.addOne(${genReadVal(types, genNullValue[V](types), isStringified, false, in)}) }

      def genArraysCopyOf[T: Type](tpe: TypeRepr, x: Expr[Array[T]], newLen: Expr[Int])(using Quotes): Expr[Array[T]] =
        if (tpe <:< TypeRepr.of[Boolean]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[Boolean]]}, $newLen) }.asExprOf[Array[T]]
        } else if (tpe <:< TypeRepr.of[Byte]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[Byte]]}, $newLen) }.asExprOf[Array[T]]
        } else if (tpe <:< TypeRepr.of[Short]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[Short]]}, $newLen) }.asExprOf[Array[T]]
        } else if (tpe <:< TypeRepr.of[Int]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[Int]]}, $newLen) }.asExprOf[Array[T]]
        } else if (tpe <:< TypeRepr.of[Long]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[Long]]}, $newLen) }.asExprOf[Array[T]]
        } else if (tpe <:< TypeRepr.of[Char]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[Char]]}, $newLen) }.asExprOf[Array[T]]
        } else if (tpe <:< TypeRepr.of[Float]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[Float]]}, $newLen) }.asExprOf[Array[T]]
        } else if (tpe <:< TypeRepr.of[Double]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[Double]]}, $newLen) }.asExprOf[Array[T]]
        } else if (tpe <:< TypeRepr.of[AnyRef]) {
          '{ java.util.Arrays.copyOf(${x.asExprOf[Array[AnyRef & T]]}, $newLen) }.asExprOf[Array[T]]
        } else fail(s"Can't find Arrays.copyOf for ${tpe.show}")

      def genReadVal[T: Type](types: List[TypeRepr], default: Expr[T], isStringified: Boolean,
                              useDiscriminator: Boolean, in: Expr[JsonReader])(using Quotes): Expr[T] = {
        val tpe = types.head
        if (traceFlag) println(s"genReadVal, tpe=${tpe.show}, useDiscriminator=$useDiscriminator")
        val implCodec = findImplicitValueCodec(types)
        val methodKey = DecoderMethodKey(tpe, isStringified && (isCollection(tpe) || isOption(tpe)), useDiscriminator)
        val decodeMethodSym = decodeMethodSyms.get(methodKey)
        if (!implCodec.isEmpty) '{ ${implCodec.get.asExprOf[JsonValueCodec[T]]}.decodeValue($in, $default) }
        else if (decodeMethodSym.isDefined) Apply(Ref(decodeMethodSym.get), List(in.asTerm, default.asTerm)).asExprOf[T]
        else if (tpe =:= TypeRepr.of[Boolean]) {
          if (isStringified) '{ $in.readStringAsBoolean() }.asExprOf[T]
          else '{ $in.readBoolean() }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.lang.Boolean]) {
          if (isStringified) '{ java.lang.Boolean.valueOf($in.readStringAsBoolean()) }.asExprOf[T]
          else '{ java.lang.Boolean.valueOf($in.readBoolean()) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[Byte]) {
          if (isStringified) '{ $in.readStringAsByte() }.asExprOf[T]
          else '{ $in.readByte() }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.lang.Byte]) {
          if (isStringified) '{ java.lang.Byte.valueOf($in.readStringAsByte()) }.asExprOf[T]
          else '{ java.lang.Byte.valueOf($in.readByte()) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[Char]) {
          '{ $in.readChar() }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.lang.Character])
          '{ java.lang.Character.valueOf($in.readChar()) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Short]) {
          if (isStringified) '{ $in.readStringAsShort() }.asExprOf[T]
          else '{ $in.readShort() }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.lang.Short]) {
          if (isStringified) '{ java.lang.Short.valueOf($in.readStringAsShort()) }.asExprOf[T]
          else '{ java.lang.Short.valueOf($in.readShort()) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[Int]) {
          if (isStringified) '{ $in.readStringAsInt() }.asExprOf[T]
          else '{ $in.readInt() }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.lang.Integer]) {
          if (isStringified) '{ java.lang.Integer.valueOf($in.readStringAsInt()) }.asExprOf[T]
          else '{ java.lang.Integer.valueOf($in.readInt()) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[Long]) {
          if (isStringified) '{ $in.readStringAsLong() }.asExprOf[T]
          else '{ $in.readLong() }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.lang.Long]) {
          if (isStringified) '{ java.lang.Long.valueOf($in.readStringAsLong()) }.asExprOf[T]
          else '{ java.lang.Long.valueOf($in.readLong()) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[Float]) {
          if (isStringified) '{ $in.readStringAsFloat() }.asExprOf[T]
          else '{ $in.readFloat() }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.lang.Float]) {
          if (isStringified) '{ java.lang.Float.valueOf($in.readStringAsFloat()) }.asExprOf[T]
          else '{ java.lang.Float.valueOf($in.readFloat()) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[Double]) {
          if (isStringified) '{ $in.readStringAsDouble() }.asExprOf[T]
          else '{ $in.readDouble() }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[java.lang.Double]) {
          if (isStringified) '{ java.lang.Double.valueOf($in.readStringAsDouble()) }.asExprOf[T]
          else '{ java.lang.Double.valueOf($in.readDouble()) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[String]) '{ $in.readString(${default.asExprOf[String]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[java.util.UUID]) '{ $in.readUUID(${default.asExprOf[java.util.UUID]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Duration]) '{ $in.readDuration(${default.asExprOf[Duration]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Instant]) '{ $in.readInstant(${default.asExprOf[Instant]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[LocalDate]) '{ $in.readLocalDate(${default.asExprOf[LocalDate]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[LocalDateTime]) '{ $in.readLocalDateTime(${default.asExprOf[LocalDateTime]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[LocalTime]) '{ $in.readLocalTime(${default.asExprOf[LocalTime]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[MonthDay]) '{ $in.readMonthDay(${default.asExprOf[MonthDay]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[OffsetDateTime]) '{ $in.readOffsetDateTime(${default.asExprOf[OffsetDateTime]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[OffsetTime]) '{ $in.readOffsetTime(${default.asExprOf[OffsetTime]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Period]) '{ $in.readPeriod(${default.asExprOf[Period]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[Year]) '{ $in.readYear(${default.asExprOf[Year]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[YearMonth]) '{ $in.readYearMonth(${default.asExprOf[YearMonth]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[ZonedDateTime]) '{ $in.readZonedDateTime(${default.asExprOf[ZonedDateTime]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[ZoneId]) '{ $in.readZoneId(${default.asExprOf[ZoneId]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[ZoneOffset]) '{ $in.readZoneOffset(${default.asExprOf[ZoneOffset]}) }.asExprOf[T]
        else if (tpe =:= TypeRepr.of[BigInt]) {
          if (isStringified) '{ $in.readStringAsBigInt(${default.asExprOf[BigInt]}, ${Expr(cfg.bigIntDigitsLimit)}) }.asExprOf[T]
          else '{ $in.readBigInt(${default.asExprOf[BigInt]}, ${Expr(cfg.bigIntDigitsLimit)}) }.asExprOf[T]
        } else if (tpe =:= TypeRepr.of[BigDecimal]) {
          val mc = withMathContextFor(cfg.bigDecimalPrecision)
          if (isStringified) {
            '{
              $in.readStringAsBigDecimal(${default.asExprOf[BigDecimal]}, $mc,
                ${Expr(cfg.bigDecimalScaleLimit)}, ${Expr(cfg.bigDecimalDigitsLimit)})
            }.asExprOf[T]
          } else {
            '{
              $in.readBigDecimal(${default.asExprOf[BigDecimal]}, $mc,
                ${Expr(cfg.bigDecimalScaleLimit)}, ${Expr(cfg.bigDecimalDigitsLimit)})
            }.asExprOf[T]
          }
        } else if (tpe =:= TypeRepr.of[Unit]) fail("Unit can't be read")
        else if (isValueClass(tpe)) {
          val tpe1 = valueClassValueType(tpe)
          tpe1.asType match
            case '[t1] =>
              val readVal = genReadVal(tpe1 :: types, genNullValue[t1](tpe1 :: types), isStringified, false, in)
              getClassInfo(tpe).genNew(List(readVal.asTerm)).asExprOf[T]
        } else if (isOption(tpe)) {
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val readVal1 = genReadVal(tpe1 :: types, genNullValue[t1](tpe1 :: types), isStringified, false, in)
              '{
                if ($in.isNextToken('n')) $in.readNullOrError($default, "expected value or null")
                else {
                  $in.rollbackToken()
                  new Some($readVal1)
                }
              }.asExprOf[T]
        } else if (tpe <:< TypeRepr.of[immutable.IntMap[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val newBuilder = withNullValueFor(tpe)(scalaCollectionEmptyNoArgs(tpe, tpe1).asExprOf[immutable.IntMap[t1]])
              val readVal = genReadVal(tpe1 :: types, genNullValue[t1](tpe1 :: types), isStringified, false, in)
              if (cfg.mapAsArray) {
                val readKey =
                  if (cfg.isStringified) '{ $in.readStringAsInt() }
                  else '{ $in.readInt() }
                genReadMapAsArray[immutable.IntMap[t1], T](newBuilder, x => Assignment('{
                  $x.updated($readKey, { if ($in.isNextToken(',')) $readVal else $in.commaError() })
                }.asTerm), _.asExprOf[T], in, default.asExprOf[T])
              } else genReadMap[immutable.IntMap[t1], immutable.IntMap[t1]](newBuilder.asExprOf[immutable.IntMap[t1]],
                x => Assignment('{ $x.updated($in.readKeyAsInt(), $readVal) }.asTerm), identity, in,
                default.asExprOf[immutable.IntMap[t1]]).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[mutable.LongMap[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val tDefault = default.asExprOf[mutable.LongMap[t1]]
              val newBuilder = '{
                if ($tDefault.isEmpty) $tDefault
                else ${scalaCollectionEmptyNoArgs(tpe, tpe1).asExprOf[mutable.LongMap[t1]]}
              }
              val readVal = genReadVal(tpe1 :: types, genNullValue[t1](tpe1 :: types), isStringified, false, in)
              if (cfg.mapAsArray) {
                val readKey =
                  if (cfg.isStringified) '{ $in.readStringAsLong() }
                  else '{ $in.readLong() }
                genReadMapAsArray[mutable.LongMap[t1], mutable.LongMap[t1]](newBuilder,
                  x => Update('{ $x.update($readKey, { if ($in.isNextToken(',')) $readVal else $in.commaError() }) }),
                  identity, in, default.asExprOf[mutable.LongMap[t1]]).asExprOf[T]
              } else {
                genReadMap[mutable.LongMap[t1], mutable.LongMap[t1]](newBuilder,
                  x => Update('{ $x.update($in.readKeyAsLong(), $readVal) }), identity, in,
                  default.asExprOf[mutable.LongMap[t1]]).asExprOf[T]
              }
        } else if (tpe <:< TypeRepr.of[immutable.LongMap[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val newBuilder = withNullValueFor(tpe)(scalaCollectionEmptyNoArgs(tpe, tpe1).asExprOf[immutable.LongMap[t1]])
              val readVal = genReadVal(tpe1 :: types, genNullValue[t1](tpe1 :: types), isStringified, false, in)
              if (cfg.mapAsArray) {
                val readKey =
                  if (cfg.isStringified) '{ $in.readStringAsLong() }
                  else '{ $in.readLong() }
                genReadMapAsArray[immutable.LongMap[t1], immutable.LongMap[t1]](newBuilder,
                  x => Assignment('{ $x.updated($readKey, { if ($in.isNextToken(',')) $readVal else $in.commaError() })}.asTerm),
                  x => x, in, default.asExprOf[immutable.LongMap[t1]]).asExprOf[T]
              } else {
                genReadMap(newBuilder, x => Assignment('{ $x.updated($in.readKeyAsLong(), $readVal) }.asTerm),
                  identity, in, default.asExprOf[immutable.LongMap[t1]]).asExprOf[T]
              }
        } else if (tpe <:< TypeRepr.of[mutable.Map[_, _]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          val tpe2 = typeArg2(tpe)
          (tpe1.asType, tpe2.asType) match
            case ('[t1], '[t2]) =>
              val tDefault = default.asExprOf[T & mutable.Map[t1, t2]]
              val newBuilder = '{
                if ($tDefault.isEmpty) $tDefault
                else ${scalaMapEmptyNoArgs(tpe, tpe1, tpe2).asExprOf[T & mutable.Map[t1, t2]]}
              }.asExprOf[T & mutable.Map[t1, t2]]

              def readVal2(using Quotes) =
                genReadVal(tpe2 :: types, genNullValue[t2](tpe2 :: types), isStringified, false, in)

              if (cfg.mapAsArray) {
                val readVal1 = genReadVal(tpe1 :: types, genNullValue[t1](tpe1 :: types), isStringified, false, in)
                genReadMapAsArray(newBuilder,
                  x => Update('{ $x.update($readVal1, { if ($in.isNextToken(',')) $readVal2 else $in.commaError() }) }),
                  identity, in, tDefault).asExprOf[T]
              } else {
                genReadMap(newBuilder, x => Update('{ $x.update(${genReadKey[t1](tpe1 :: types, in)}, $readVal2) }),
                  identity, in, tDefault).asExprOf[T]
              }
        } else if (tpe <:< TypeRepr.of[collection.Map[_, _]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          val tpe2 = typeArg2(tpe)
          (tpe1.asType, tpe2.asType) match
            case ('[t1], '[t2]) =>
              val builderNoApply =
                TypeApply(Select.unique(scalaCollectionCompanion(tpe), "newBuilder"), List(TypeTree.of[t1], TypeTree.of[t2]))
              val newBuilder =
                (if (tpe <:< TypeRepr.of[immutable.SortedMap[_, _]]) Apply(builderNoApply, List(summonOrdering(tpe1))) // TODO: add withOrderfingFoe
                else builderNoApply).asExprOf[mutable.Builder[(t1, t2), T & collection.Map[t1, t2]]]

              def readVal2(using Quotes) =
                genReadVal(tpe2 :: types, genNullValue[t2](tpe2 :: types), isStringified, false, in)

              if (cfg.mapAsArray) {
                def readVal1(using Quotes) =
                  genReadVal(tpe1 :: types, genNullValue[t1](tpe1 :: types), isStringified, false, in)

                def readKV(using Quotes)(x: Expr[mutable.Builder[(t1, t2), collection.Map[t1, t2]]]) =
                  Update('{ $x.addOne(($readVal1, { if ($in.isNextToken(',')) $readVal2 else $in.commaError() }))})

                genReadMapAsArray(newBuilder, readKV, (b) => '{ $b.result() }, in, default).asExprOf[T]
              } else {
                def readKey(using Quotes) = genReadKey[t1](tpe1 :: types, in)

                def readKV(using Quotes)(x: Expr[mutable.Builder[(t1, t2), T & collection.Map[t1, t2]]]) =
                  Update('{ $x.addOne(($readKey, $readVal2)) })

                genReadMap[mutable.Builder[(t1, t2), T & collection.Map[t1, t2]], T & collection.Map[t1, t2]](newBuilder,
                  readKV, (b) => '{ $b.result() }, in, default.asExprOf[T & collection.Map[t1, t2]]).asExprOf[T]
              }
        } else if (tpe <:< TypeRepr.of[BitSet]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val readVal = if (isStringified) '{ $in.readStringAsInt() }  else '{ $in.readInt() }
          '{
            if ($in.isNextToken('[')) {
              if ($in.isNextToken(']')) $default
              else {
                $in.rollbackToken()
                var x = new Array[Long](2)
                while ({
                  val v = $readVal
                  if (v < 0 || v >= ${Expr(cfg.bitSetValueLimit)}) $in.decodeError("illegal value for bit set")
                  val xi = v >>> 6
                  if (xi >= x.length) x = java.util.Arrays.copyOf(x, java.lang.Integer.highestOneBit(xi) << 1)
                  x(xi) |= 1L << v
                  $in.isNextToken(',')
                }) ()
                if ($in.isCurrentToken(']')) ${
                  if (tpe =:= TypeRepr.of[BitSet] || tpe =:= TypeRepr.of[immutable.BitSet]) '{ immutable.BitSet.fromBitMaskNoCopy(x) }
                  else '{ mutable.BitSet.fromBitMaskNoCopy(x) }
                } else $in.arrayEndOrCommaError()
              }
            } else $in.readNullOrTokenError($default, '[')
          }.asExprOf[T]
        } else if (tpe <:< TypeRepr.of[mutable.Set[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val tDefault = default.asExprOf[T & mutable.Set[t1]]
              val emptySetNoOrdering = scalaCollectionEmptyNoArgs(tpe, tpe1)
              val emptySet =
                (if (tpe <:< TypeRepr.of[mutable.SortedSet[_]]) Apply(emptySetNoOrdering, List(summonOrdering(tpe1)))
                else emptySetNoOrdering).asExprOf[T & mutable.Set[t1]]
              genReadSet('{
                if ($tDefault.isEmpty) $tDefault
                else $emptySet
              }, x => Update(genReadValForGrowable(tpe1 :: types, isStringified, x, in)), tDefault, identity, in).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[collection.Set[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val builderNoOrdering =
                TypeApply(Select.unique(scalaCollectionCompanion(tpe), "newBuilder"), List(TypeTree.of[t1]))
              val builder =
                (if (tpe <:< TypeRepr.of[collection.SortedSet[_]]) Apply(builderNoOrdering, List(summonOrdering(tpe1)))
                else builderNoOrdering).asExprOf[mutable.Builder[t1, T & collection.Set[t1]]]
              genReadSet(builder, b => Update(genReadValForGrowable(tpe1 :: types, isStringified, b, in)),
                default.asExprOf[T & collection.Set[t1]], b => '{ $b.result() }, in).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[::[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match {
            case '[t1] =>
              val tDefault = default.asExprOf[::[t1]]
              '{
                if ($in.isNextToken('[')) {
                  if ($in.isNextToken(']')) {
                    if ($tDefault ne null) $tDefault
                    else $in.decodeError("expected non-empty JSON array")
                  } else {
                    $in.rollbackToken()
                    val x = new mutable.ListBuffer[t1]
                    while ({
                      ${genReadValForGrowable(tpe1 :: types, isStringified, 'x, in)}
                      $in.isNextToken(',')
                    }) ()
                    if ($in.isCurrentToken(']')) x.toList.asInstanceOf[::[t1]]
                    else $in.arrayEndOrCommaError()
                  }
                } else {
                  if ($tDefault ne null) $in.readNullOrTokenError($tDefault, '[')
                  else $in.decodeError("expected non-empty JSON array")
                }
              }.asExprOf[T]
          }
        } else if (tpe <:< TypeRepr.of[List[_]] || tpe =:= TypeRepr.of[Seq[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              genReadArray('{ new mutable.ListBuffer[t1] },
                (x, _) => Update(genReadValForGrowable(tpe1 :: types, isStringified, x, in)),
                default.asExprOf[List[t1]], (x, _) => '{ $x.toList }, in).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[mutable.ListBuffer[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val tDefault = default.asExprOf[mutable.ListBuffer[t1]]
              genReadArray('{
                if ($tDefault.isEmpty) $tDefault
                else new mutable.ListBuffer[t1]
              }, (x, _) => Update(genReadValForGrowable(tpe1 :: types, isStringified, x, in)), tDefault, (x, _) => x, in).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[mutable.Iterable[_] with mutable.Growable[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val emptyCollection = {
                if (tpe <:< TypeRepr.of[mutable.ArraySeq[_]] || tpe <:< TypeRepr.of[mutable.UnrolledBuffer[_]]) {
                  Apply(scalaCollectionEmptyNoArgs(tpe, tpe1), List(summonClassTag(tpe1)))
                } else scalaCollectionEmptyNoArgs(tpe, tpe1)
              }.asExprOf[T & mutable.Growable[t1]]
              genReadArray('{
                if (${default.asExprOf[Iterable[_]]}.isEmpty) $default
                else $emptyCollection
              }.asExprOf[T & mutable.Growable[t1]], (x, _) => Update(genReadValForGrowable(tpe1 :: types, isStringified, x, in)),
                default, (x, _) => x, in).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[Iterable[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val builder = TypeApply(Select.unique(scalaCollectionCompanion(tpe), "newBuilder"), List(TypeTree.of[t1]))
              genReadArray({
                if (tpe <:< TypeRepr.of[mutable.ArraySeq[_]]) Apply(builder, List(summonClassTag(tpe1)))
                else builder
              }.asExprOf[mutable.Builder[t1, T]], (x, _) => Update(genReadValForGrowable(tpe1 :: types, isStringified, x, in)),
                default, (x, _) => '{ $x.result() }, in)
        } else if (tpe <:< TypeRepr.of[Array[_]]) withDecoderFor(methodKey, default, in) { (in, default) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val newArrayOnChange = tpe1 match
                case AppliedType(_, _) => true
                case _ => isValueClass(tpe1)
              val t1ClassTag = summonClassTag(tpe1).asExprOf[ClassTag[t1]]

              def growArray(x: Expr[Array[t1]], i: Expr[Int])(using Quotes): Expr[Array[t1]] =
                if (newArrayOnChange) '{
                  val x1 = new Array[t1]($i << 1)(using $t1ClassTag)
                  java.lang.System.arraycopy($x, 0, x1, 0, $i)
                  x1
                } else genArraysCopyOf[t1](tpe1, x, '{ $i << 1 })

              def shrinkArray(x: Expr[Array[t1]], i: Expr[Int])(using Quotes): Expr[Array[t1]] =
                if (newArrayOnChange) '{
                  val x1 = new Array[t1]($i)(using $t1ClassTag)
                  java.lang.System.arraycopy($x, 0, x1, 0, $i)
                  x1
                } else genArraysCopyOf[t1](tpe1, x, i)

              val tDefault = default.asExprOf[Array[t1]]
              genReadArray('{ new Array[t1](16)(using $t1ClassTag) },
                (x, i) => ConditionalAssignmentAndUpdate('{ ($i == $x.length) }, growArray(x, i).asTerm, '{
                  $x($i) = ${genReadVal(tpe1 :: types, genNullValue[t1](tpe1 :: types), isStringified, false, in)}
                }), tDefault, (x, i) => '{
                  if ($i == $x.length) $x
                  else ${shrinkArray(x, i)}
                }.asExprOf[Array[t1]], in).asExprOf[T]
        } else if (tpe <:< TypeRepr.of[Enumeration#Value]) withDecoderFor(methodKey, default, in) { (in, default) =>
          if (cfg.useScalaEnumValueId) {
            val ec =
              if (MacroUtils.isNative) withScala2EnumerationSingleThreadCacheFor[Int, T & Enumeration#Value](tpe)
              else withScala2EnumerationConcurrentCacheFor[Int, T & Enumeration#Value](tpe)
            if (isStringified) '{
              if ($in.isNextToken('"')) {
                $in.rollbackToken()
                val i = $in.readStringAsInt()
                var x = $ec.get(i)
                if (x eq null) {
                  x = ${findScala2EnumerationById[T & Enumeration#Value](tpe, 'i)}.getOrElse($in.enumValueError(i.toString))
                  $ec.put(i, x)
                }
                x
              } else $in.readNullOrTokenError($default, '"')
            } else '{
              val t = $in.nextToken()
              if (t >= '0' && t <= '9') {
                $in.rollbackToken()
                val i = $in.readInt()
                var x = $ec.get(i)
                if (x eq null) {
                  x = ${findScala2EnumerationById[T & Enumeration#Value](tpe, 'i)}.getOrElse($in.decodeError("illegal enum value " + i))
                  $ec.put(i, x)
                }
                x
              } else $in.readNullOrError($default, "expected digit")
            }
          } else {
            val ec =
              if (MacroUtils.isNative) withScala2EnumerationSingleThreadCacheFor[String, T & Enumeration#Value](tpe)
              else withScala2EnumerationConcurrentCacheFor[String, T & Enumeration#Value](tpe)
            '{
              if ($in.isNextToken('"')) {
                $in.rollbackToken()
                val s = $in.readString(null)
                var x = $ec.get(s)
                if (${'x.asExprOf[AnyRef]} eq null) {
                  x = ${findScala2EnumerationByName[T & Enumeration#Value](tpe,'s)}.getOrElse($in.enumValueError(s.length))
                  $ec.put(s, x)
                }
                x
              } else $in.readNullOrTokenError($default, '"')
            }
          }
        } else if (isJavaEnum(tpe)) withDecoderFor(methodKey, default, in) { (in, default) =>
          '{
            if ($in.isNextToken('"')) {
              $in.rollbackToken()
              val l = $in.readStringAsCharBuf()
              ${genReadJavaEnumValue(javaEnumValues(tpe), '{ $in.enumValueError(l) }, in, 'l) }
            } else $in.readNullOrTokenError($default, '"')
          }
        } else if (isEnumOrModuleValue(tpe)) withDecoderFor(methodKey, default, in) { (in, default) =>
          '{
            if ($in.isNextToken('{')) {
              $in.rollbackToken()
              $in.skip()
              ${Ref(tpe.termSymbol).asExprOf[T]}
            } else $in.readNullOrTokenError($default, '{')
          }
        } else if (isTuple(tpe)) withDecoderFor(methodKey, default, in) { (in, default) =>
          val valDefs = new ArrayBuffer[ValDef]
          val indexedTypes = typeArgs(tpe).zipWithIndex
          indexedTypes.foreach { case (te, i) =>
            te.asType match
              case '[t] =>
                val sym = symbol("_r" + (i + 1), te)
                val nullVal = genNullValue[t](te :: types)
                val rhs =
                  if (i == 0) genReadVal(te :: types, nullVal, isStringified, false, in)
                  else '{
                    if ($in.isNextToken(',')) {
                      ${genReadVal(te :: types, nullVal, isStringified, false, in)}
                    } else $in.commaError()
                  }
                valDefs.addOne(ValDef(sym, Some(rhs.asTerm.changeOwner(sym))))
          }
          val readCreateBlock = Block(valDefs.toList, '{
            if ($in.isNextToken(']'))
              ${Apply(TypeApply(Select.unique(New(Inferred(tpe)), ""),
                indexedTypes.map(x => Inferred(x._1)).toList), valDefs.map(x => Ref(x.symbol)).toList).asExpr}
            else $in.arrayEndError()
          }.asTerm)
          '{
            if ($in.isNextToken('[')) ${readCreateBlock.asExprOf[T]}
            else $in.readNullOrTokenError($default, '[')
          }
        } else if (isSealedClass(tpe)) withDecoderFor(methodKey, default, in) { (in, default) =>
          genReadSealedClass(types, in, default, isStringified)
        } else if (isNonAbstractScalaClass(tpe)) withDecoderFor(methodKey, default, in) { (in, default) =>
          genReadNonAbstractScalaClass(types, useDiscriminator, in, default)
        } else if (isConstType(tpe)) genReadConstType(tpe, isStringified, in)
        else cannotFindValueCodecError(tpe)
      }

      def genWriteNonAbstractScalaClass[T: Type](x: Expr[T], types: List[TypeRepr],
                                                 optDiscriminator: Option[WriteDiscriminator],
                                                 out: Expr[JsonWriter])(using Quotes): Expr[Unit] = {
        val tpe = types.head
        if (traceFlag) println(s"genWriteNonAbstractScalaClass[${tpe.show}]")
        val classInfo = getClassInfo(tpe)
        val writeFields = classInfo.nonTransientFields.map { f =>
          val fDefault =
            if (cfg.transientDefault) f.defaultValue
            else None
          f.resolvedTpe.asType match {
            case '[ft] =>
              fDefault match {
                case Some(d) =>
                  if (f.resolvedTpe <:< TypeRepr.of[Iterable[_]] && cfg.transientEmpty) {
                    val tpe1 = typeArg1(f.resolvedTpe.baseType(TypeRepr.of[Iterable[_]].typeSymbol))
                    tpe1.asType match
                      case '[t1] => '{
                        val v = ${f.genGet(x.asTerm).asExprOf[Iterable[t1]]}
                        if (!v.isEmpty && v != ${d.asExprOf[ft]}) {
                          ${genWriteConstantKey(f.mappedName, out)}
                          ${genWriteVal('v, TypeRepr.of[Iterable[t1]] :: types, f.isStringified, None, out)}
                        }
                      }.asExprOf[Unit]
                  } else if (isOption(f.resolvedTpe) && cfg.transientNone) {
                    val tpe1 = typeArg1(f.resolvedTpe)
                    tpe1.asType match
                      case '[t1] => '{
                        val v = ${f.genGet(x.asTerm).asExprOf[Option[t1]]}
                        if ((v ne None) && v != ${d.asExprOf[ft]}) {
                          ${genWriteConstantKey(f.mappedName, out)}
                          ${genWriteVal('{v.get}, tpe1 :: types, f.isStringified, None, out)}
                        }
                      }
                  } else if (f.resolvedTpe <:< TypeRepr.of[Array[_]]) {
                    def cond(v: Expr[Array[_]])(using Quotes): Expr[Boolean] =
                      val da = d.asExprOf[Array[_]]
                      if (cfg.transientEmpty)
                        '{ $v.length > 0 && !${withEqualsFor(f.resolvedTpe, v, da)((x1, x2) => genArrayEquals(f.resolvedTpe, x1, x2))} }
                      else
                        '{ !${withEqualsFor(f.resolvedTpe, v, da)((x1, x2) => genArrayEquals(f.resolvedTpe, x1, x2))} }

                    '{
                      val v = ${f.genGet(x.asTerm).asExprOf[ft & Array[_]]}
                      if (${cond('v)}) {
                        ${genWriteConstantKey(f.mappedName, out)}
                        ${genWriteVal('v, f.resolvedTpe :: types, f.isStringified, None, out)}
                      }
                    }
                  } else '{
                    val v = ${f.genGet(x.asTerm).asExprOf[ft]}
                    if (v != ${d.asExprOf[ft]}) {
                      ${genWriteConstantKey(f.mappedName, out)}
                      ${genWriteVal('v, f.resolvedTpe :: types, f.isStringified, None, out)}
                    }
                  }
                case None =>
                  if (f.resolvedTpe <:< TypeRepr.of[Iterable[_]] && cfg.transientEmpty) '{
                    val v = ${f.genGet(x.asTerm).asExprOf[ft & Iterable[_]]}
                    if (!v.isEmpty) {
                      ${genWriteConstantKey(f.mappedName, out)}
                      ${genWriteVal('v, f.resolvedTpe :: types, f.isStringified, None, out)}
                    }
                  } else if (isOption(f.resolvedTpe) && cfg.transientNone) {
                    val tpe1 = typeArg1(f.resolvedTpe)
                    tpe1.asType match
                      case '[tf] => '{
                        val v = ${f.genGet(x.asTerm).asExprOf[Option[tf]]}
                        if (v ne None) {
                          ${genWriteConstantKey(f.mappedName, out)}
                          ${genWriteVal('{ v.get }, tpe1 :: types, f.isStringified, None, out)}
                        }
                      }
                  } else if (f.resolvedTpe <:< TypeRepr.of[Array[_]] && cfg.transientEmpty) '{
                    val v = ${f.genGet(x.asTerm).asExprOf[ft & Array[_]]}
                    if (v.length > 0) {
                      ${genWriteConstantKey(f.mappedName, out)}
                      ${genWriteVal('v, f.resolvedTpe :: types, f.isStringified, None, out)}
                    }
                  } else '{
                    ${genWriteConstantKey(f.mappedName, out)}
                    ${genWriteVal(f.genGet(x.asTerm).asExprOf[ft], f.resolvedTpe :: types, f.isStringified, None, out)}
                  }
              }
          }
        }
        val allWriteFields = optDiscriminator.fold(writeFields)(_.write(out) +: writeFields)
        Block('{ $out.writeObjectStart() }.asTerm :: allWriteFields.toList.map(_.asTerm),
          '{ $out.writeObjectEnd() }.asTerm).asExprOf[Unit]
      }

      def getWriteConstType(tpe: TypeRepr, m: Term, isStringified: Boolean, out: Expr[JsonWriter])(using Quotes): Expr[Unit] = tpe match
        case ConstantType(StringConstant(_)) => '{ $out.writeVal(${m.asExprOf[String]}) }
        case ConstantType(BooleanConstant(_)) =>
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Boolean]}) }
          else '{ $out.writeVal(${m.asExprOf[Boolean]}) }
        case ConstantType(ByteConstant(_)) =>
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Byte]}) }
          else '{ $out.writeVal(${m.asExprOf[Boolean]}) }
        case ConstantType(CharConstant(v)) => '{ $out.writeVal(${Expr(v)}) }
        case ConstantType(ShortConstant(v)) =>
          if (isStringified) '{ $out.writeValAsString(${Expr(v)}) }
          else '{ $out.writeVal(${Expr(v)}) }
        case ConstantType(IntConstant(v)) =>
          if (isStringified) '{ $out.writeValAsString(${Expr(v)}) }
          else '{ $out.writeVal(${Expr(v)}) }
        case ConstantType(LongConstant(v)) =>
          if (isStringified) '{ $out.writeValAsString(${Expr(v)}) }
          else '{ $out.writeVal(${Expr(v)}) }
        case ConstantType(FloatConstant(v)) =>
          if (isStringified) '{ $out.writeValAsString(${Expr(v)}) }
          else '{ $out.writeVal(${Expr(v)}) }
        case ConstantType(DoubleConstant(v)) =>
          if (isStringified) '{ $out.writeValAsString(${Expr(v)}) }
          else '{ $out.writeVal(${Expr(v)}) }
        case _ => cannotFindValueCodecError(tpe)

      def genWriteVal[T: Type](m: Expr[T], types: List[TypeRepr], isStringified: Boolean,
                               optWriteDiscriminator: Option[WriteDiscriminator],
                               out: Expr[JsonWriter])(using Quotes): Expr[Unit]= {
        val tpe = types.head
        if (traceFlag) println(s"genWriteVal(${tpe.show})")
        val implCodec = findImplicitValueCodec(types)
        val methodKey = EncoderMethodKey(tpe, isStringified && (isCollection(tpe) || isOption(tpe)),
            optWriteDiscriminator.map(x => (x.fieldName, x.fieldValue)))
        val encodeMethodSym = encodeMethodSyms.get(methodKey)
        if (!implCodec.isEmpty) '{ ${implCodec.get.asExprOf[JsonValueCodec[T]]}.encodeValue($m, $out) }
        else if (encodeMethodSym.isDefined) Apply(Ref(encodeMethodSym.get), List(m.asTerm, out.asTerm)).asExprOf[Unit]
        else if (tpe <:< TypeRepr.of[Boolean]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Boolean]}) }
          else '{ $out.writeVal(${m.asExprOf[Boolean]}) }
        } else if (tpe =:= TypeRepr.of[java.lang.Boolean]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[java.lang.Boolean]}) }
          else '{ $out.writeVal(${m.asExprOf[java.lang.Boolean]}) }
        } else if (tpe <:< TypeRepr.of[Byte]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Byte]}) }
          else '{ $out.writeVal(${m.asExprOf[Byte]}) }
        } else if (tpe =:= TypeRepr.of[java.lang.Byte]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[java.lang.Byte]}.byteValue) }
          else '{ $out.writeVal(${m.asExprOf[java.lang.Byte]}.byteValue) }
        } else if (tpe <:< TypeRepr.of[Short]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Short]}) }
          else '{ $out.writeVal(${m.asExprOf[Short]}) }
        } else if (tpe =:= TypeRepr.of[java.lang.Short]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[java.lang.Short]}.shortValue) }
          else '{ $out.writeVal(${m.asExprOf[java.lang.Short]}.shortValue) }
        } else if (tpe <:< TypeRepr.of[Int]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Int]}) }
          else '{ $out.writeVal(${m.asExprOf[Int]}) }
        } else if (tpe =:= TypeRepr.of[java.lang.Integer]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[java.lang.Integer]}.intValue) }
          else '{ $out.writeVal(${m.asExprOf[java.lang.Integer]}.intValue) }
        } else if (tpe <:< TypeRepr.of[Long]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Long]}) }
          else '{ $out.writeVal(${m.asExprOf[Long]}) }
        } else if (tpe =:= TypeRepr.of[java.lang.Long]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[java.lang.Long]}.longValue) }
          else '{ $out.writeVal(${m.asExprOf[java.lang.Long]}.longValue) }
        } else if (tpe =:= TypeRepr.of[Float]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Float]}) }
          else '{ $out.writeVal(${m.asExprOf[Float]}) }
        } else if (tpe =:= TypeRepr.of[java.lang.Float]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[java.lang.Float]}.floatValue) }
          else '{ $out.writeVal(${m.asExprOf[java.lang.Float]}.floatValue) }
        } else if (tpe =:= TypeRepr.of[Double]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[Double]}) }
          else '{ $out.writeVal(${m.asExprOf[Double]}) }
        } else if (tpe =:= TypeRepr.of[java.lang.Double]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[java.lang.Double]}.doubleValue) }
          else '{ $out.writeVal(${m.asExprOf[java.lang.Double]}.doubleValue) }
        } else if (tpe =:= TypeRepr.of[BigInt]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[BigInt]}) }
          else '{ $out.writeVal(${m.asExprOf[BigInt]}) }
        } else if (tpe =:= TypeRepr.of[BigDecimal]) {
          if (isStringified) '{ $out.writeValAsString(${m.asExprOf[BigDecimal]}) }
          else '{ $out.writeVal(${m.asExprOf[BigDecimal]}) }
        } else if (tpe =:= TypeRepr.of[Char]) '{ $out.writeVal(${m.asExprOf[Char]}) }
        else if (tpe =:= TypeRepr.of[java.lang.Character]) '{ $out.writeVal(${m.asExprOf[java.lang.Character]}) }
        else if (tpe =:= TypeRepr.of[String]) '{ $out.writeVal(${m.asExprOf[String]}) }
        else if (tpe =:= TypeRepr.of[java.util.UUID]) '{ $out.writeVal(${m.asExprOf[java.util.UUID]}) }
        else if (tpe =:= TypeRepr.of[Duration]) '{ $out.writeVal(${m.asExprOf[Duration]}) }
        else if (tpe =:= TypeRepr.of[Instant]) '{ $out.writeVal(${m.asExprOf[Instant]}) }
        else if (tpe =:= TypeRepr.of[LocalDate]) '{ $out.writeVal(${m.asExprOf[LocalDate]}) }
        else if (tpe =:= TypeRepr.of[LocalDateTime]) '{ $out.writeVal(${m.asExprOf[LocalDateTime]}) }
        else if (tpe =:= TypeRepr.of[LocalTime]) '{ $out.writeVal(${m.asExprOf[LocalTime]}) }
        else if (tpe =:= TypeRepr.of[MonthDay]) '{ $out.writeVal(${m.asExprOf[MonthDay]}) }
        else if (tpe =:= TypeRepr.of[OffsetDateTime]) '{ $out.writeVal(${m.asExprOf[OffsetDateTime]}) }
        else if (tpe =:= TypeRepr.of[OffsetTime]) '{ $out.writeVal(${m.asExprOf[OffsetTime]}) }
        else if (tpe =:= TypeRepr.of[Period]) '{ $out.writeVal(${m.asExprOf[Period]}) }
        else if (tpe =:= TypeRepr.of[Year]) '{ $out.writeVal(${m.asExprOf[Year]}) }
        else if (tpe =:= TypeRepr.of[YearMonth]) '{ $out.writeVal(${m.asExprOf[YearMonth]}) }
        else if (tpe =:= TypeRepr.of[ZonedDateTime]) '{ $out.writeVal(${m.asExprOf[ZonedDateTime]}) }
        else if (tpe =:= TypeRepr.of[ZoneId]) '{ $out.writeVal(${m.asExprOf[ZoneId]}) }
        else if (tpe =:= TypeRepr.of[ZoneOffset]) '{ $out.writeVal(${m.asExprOf[ZoneOffset]}) }
        else if (isValueClass(tpe)) {
          val vtpe = valueClassValueType(tpe)
          genWriteVal(Select(m.asTerm, valueClassValue(tpe)).asExpr, vtpe :: types, isStringified, None, out)
        } else if (isOption(tpe)) {
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] => '{
              ${m.asExprOf[Option[t1]]} match
                case Some(x) => ${genWriteVal('x, tpe1 :: types, isStringified, None, out)}
                case None => $out.writeNull()
            }
        } else if (tpe <:< TypeRepr.of[immutable.IntMap[_]] || tpe <:< TypeRepr.of[mutable.LongMap[_]] ||
            tpe <:< TypeRepr.of[immutable.LongMap[_]]) withEncoderFor(methodKey, m, out) { (out, x) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              def writeVal2(out: Expr[JsonWriter], v: Expr[t1])(using Quotes) =
                genWriteVal(v, tpe1 :: types, isStringified, None, out)

              if (tpe <:< TypeRepr.of[immutable.IntMap[_]]) {
                val tx = x.asExprOf[immutable.IntMap[t1]]
                if (cfg.mapAsArray) {
                  def writeVal1(out: Expr[JsonWriter], k: Expr[Int])(using Quotes): Expr[Unit] =
                    if (isStringified) '{ $out.writeValAsString($k) }
                    else '{ $out.writeVal($k) }

                  genWriteMapAsArrayScala213(tx, writeVal1, writeVal2, out)
                } else genWriteMapScala213(tx, (out, k) => '{ $out.writeKey($k) }, writeVal2, out)
              } else {
                val tx = x.asExprOf[collection.Map[Long, t1]]
                if (cfg.mapAsArray) {
                  def writeVal1(out: Expr[JsonWriter], k: Expr[Long])(using Quotes): Expr[Unit] =
                    if (isStringified) '{ $out.writeValAsString($k) }
                    else '{ $out.writeVal($k) }

                  genWriteMapAsArrayScala213(tx, writeVal1, writeVal2, out)
                } else genWriteMapScala213(tx, (out, k) => '{ $out.writeKey($k) }, writeVal2, out)
              }
        } else if (tpe <:< TypeRepr.of[collection.Map[_, _]]) withEncoderFor(methodKey, m, out) { (out, x) =>
          val tpe1 = typeArg1(tpe)
          val tpe2 = typeArg2(tpe)
          (tpe1.asType, tpe2.asType) match
            case ('[t1], '[t2]) =>
              def writeVal2(out: Expr[JsonWriter], v: Expr[t2])(using Quotes): Expr[Unit] =
                genWriteVal(v, tpe2 :: types, isStringified, None, out)

              val tx = x.asExprOf[collection.Map[t1, t2]]
              if (cfg.mapAsArray) {
                genWriteMapAsArrayScala213(tx, (out, k) => genWriteVal(k, tpe1 :: types, isStringified, None, out), writeVal2, out)
              } else genWriteMapScala213(tx, (out, k) => genWriteKey(k, tpe1 :: types, out), writeVal2, out)
        } else if (tpe <:< TypeRepr.of[BitSet]) withEncoderFor(methodKey, m, out) { (out, x) =>
          genWriteArray(x.asExprOf[BitSet], (out, x1) => {
            if (isStringified) '{ $out.writeValAsString($x1) }
            else '{ $out.writeVal($x1) }
          }, out)
        } else if (tpe <:< TypeRepr.of[List[_]]) withEncoderFor(methodKey, m, out) { (out, x) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val tx = x.asExprOf[List[t1]]
              '{
                $out.writeArrayStart()
                var l = $tx
                while (l ne Nil) {
                  ${genWriteVal('{ l.head }, tpe1 :: types, isStringified, None, out)}
                  l = l.tail
                }
                $out.writeArrayEnd()
              }
        } else if (tpe <:< TypeRepr.of[IndexedSeq[_]]) withEncoderFor(methodKey, m, out) { (out, x) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val tx = x.asExprOf[IndexedSeq[t1]]
              '{
                $out.writeArrayStart()
                val l = $tx.size
                var i = 0
                while (i < l) {
                  ${genWriteVal('{ $tx(i) }, tpe1 :: types, isStringified, None, out)}
                  i += 1
                }
                $out.writeArrayEnd()
              }
        } else if (tpe <:< TypeRepr.of[Iterable[_]]) withEncoderFor(methodKey, m, out) { (out, x) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              genWriteArray(x.asExprOf[Iterable[t1]],
                (out, x1) => genWriteVal(x1, tpe1 :: types, isStringified, None, out), out)
        } else if (tpe <:< TypeRepr.of[Array[_]]) withEncoderFor(methodKey, m, out) { (out, x) =>
          val tpe1 = typeArg1(tpe)
          tpe1.asType match
            case '[t1] =>
              val tx = x.asExprOf[Array[t1]]
              '{
                $out.writeArrayStart()
                val l = $tx.length
                var i = 0
                while (i < l) {
                  ${genWriteVal('{ $tx(i) }, tpe1 :: types, isStringified, None, out)}
                  i += 1
                }
                $out.writeArrayEnd()
              }
        } else if (tpe <:< TypeRepr.of[Enumeration#Value]) withEncoderFor(methodKey, m, out) { (out, x) =>
          val tx = x.asExprOf[Enumeration#Value]
          if (cfg.useScalaEnumValueId) {
            if (isStringified) '{ $out.writeValAsString($tx.id) }
            else '{ $out.writeVal($tx.id) }
          } else '{ $out.writeVal($tx.toString) }
        } else if (isJavaEnum(tpe)) withEncoderFor(methodKey, m, out) { (out, x) =>
          val es = javaEnumValues(tpe)
          val encodingRequired = es.exists(e => isEncodingRequired(e.name))
          if (es.exists(_.transformed)) {
            val cases = es.map(e => CaseDef(Ref(e.value), None, Expr(e.name).asTerm)) :+
              CaseDef('{ null }.asTerm, None, '{ $out.encodeError("illegal enum value: null") }.asTerm)
            val matching = Match(x.asTerm, cases.toList).asExprOf[String]
            if (encodingRequired) '{ $out.writeVal($matching) }
            else '{ $out.writeNonEscapedAsciiVal($matching) }
          } else {
            val tx = x.asExprOf[java.lang.Enum[_]]
            if (encodingRequired) '{ $out.writeVal($tx.name) }
            else '{ $out.writeNonEscapedAsciiVal($tx.name) }
          }
        } else if (isEnumOrModuleValue(tpe)) withEncoderFor(methodKey, m, out) { (out, x) =>
          '{
            $out.writeObjectStart()
            ${optWriteDiscriminator.fold('{})(_.write(out))}
            $out.writeObjectEnd()
          }
        } else if (isTuple(tpe)) withEncoderFor(methodKey, m, out) { (out, x) =>
          val writeFields = typeArgs(tpe).zipWithIndex.map { case (te, i) =>
            te.asType match
              case '[t] =>
                genWriteVal(Select.unique(x.asTerm, "_" + (i + 1)).asExprOf[t], te :: types, isStringified, None, out).asTerm
          }
          if (writeFields.isEmpty) fail(s"Expected that ${tpe.show} should be an applied type")
          Block('{ $out.writeArrayStart() }.asTerm :: writeFields.toList,
            '{ $out.writeArrayEnd() }.asTerm).asExprOf[Unit]
        } else if (isSealedClass(tpe)) withEncoderFor(methodKey, m, out) { (out, x) =>
          def genWriteLeafClass(subTpe: TypeRepr, discriminator: Option[WriteDiscriminator], vx: Term): Expr[Unit] =
            subTpe.asType match
              case '[st] =>
                if (subTpe =:= tpe) genWriteNonAbstractScalaClass(vx.asExprOf[st], types, discriminator, out)
                else genWriteVal(vx.asExprOf[st], subTpe :: types, isStringified, discriminator, out)

          val leafClasses = adtLeafClasses(tpe)
          val writeSubclasses = cfg.discriminatorFieldName.fold {
            val (leafModuleClasses, leafCaseClasses) = leafClasses.partition(x => isEnumOrModuleValue(x))
            leafCaseClasses.map { subTpe =>
              val vxSym = Symbol.newBind(Symbol.spliceOwner, "vx", Flags.EmptyFlags, subTpe)
              CaseDef(Bind(vxSym, Typed(Wildcard(), Inferred(subTpe))), None, '{
                $out.writeObjectStart()
                ${genWriteConstantKey(discriminatorValue(subTpe), out)}
                ${genWriteLeafClass(subTpe, None, Ref(vxSym))}
                $out.writeObjectEnd()
              }.asTerm)
            } ++ leafModuleClasses.map { subTpe =>
              CaseDef(Typed(x.asTerm, Inferred(subTpe)), None,
                genWriteConstantVal(discriminatorValue(subTpe), out).asTerm)
            }
          } { discrFieldName =>
            leafClasses.map { subTpe =>
              val vxSym = Symbol.newBind(Symbol.spliceOwner, "vx", Flags.EmptyFlags, subTpe)
              val writeDiscriminator = WriteDiscriminator(discrFieldName, discriminatorValue(subTpe))
              CaseDef(Bind(vxSym, Typed(Wildcard(), Inferred(subTpe))), None,
                genWriteLeafClass(subTpe, Some(writeDiscriminator), Ref(vxSym)).asTerm)
            }
          } :+ CaseDef(Literal(NullConstant()), None, '{ $out.writeNull() }.asTerm)
          Match(x.asTerm, writeSubclasses.toList).asExprOf[Unit]
        } else if (isNonAbstractScalaClass(tpe)) withEncoderFor(methodKey, m, out) { (out, x) =>
          genWriteNonAbstractScalaClass(x.asExprOf[T], types, optWriteDiscriminator, out)
        } else if (isConstType(tpe)) getWriteConstType(tpe, m.asTerm, isStringified, out)
        else cannotFindValueCodecError(tpe)
      }

      val codecDef = '{
        new JsonValueCodec[A] {
          def nullValue: A = ${genNullValue[A](rootTpe :: Nil)}

          def decodeValue(in: JsonReader, default: A): A =
            ${genReadVal(rootTpe :: Nil, 'default, cfg.isStringified, false, 'in)}

          def encodeValue(x: A, out: JsonWriter): Unit =
            ${genWriteVal('x, rootTpe :: Nil, cfg.isStringified, None, 'out)}
        }
      }.asTerm
      val needDefs =
        mathContexts.values ++
          nullValues.values ++
          equalsMethods.values ++
          scala2EnumerationCaches.values ++
          fieldIndexAccessors.values ++
          decodeMethodDefs.values ++
          encodeMethodDefs.values
      val codec = Block(needDefs.toList, codecDef).asExprOf[JsonValueCodec[A]]
      if (//TODO: uncomment after graduating from experimental API: CompilationInfo.XmacroSettings.contains("print-codecs") ||
        Expr.summon[CodecMakerConfig.PrintCodec].isDefined) {
        report.info(s"Generated JSON codec for type '${rootTpe.show}':\n${codec.show}", Position.ofMacroExpansion)
      }
      codec
    }
  }

  private[this] def isEncodingRequired(s: String): Boolean =
    val len = s.length
    var i = 0
    while (i < len && JsonWriter.isNonEscapedAscii(s.charAt(i))) i += 1
    i != len

  private[this] def groupByOrdered[A, K](xs: collection.Seq[A])(f: A => K): collection.Seq[(K, collection.Seq[A])] =
    xs.foldLeft(new mutable.LinkedHashMap[K, ArrayBuffer[A]]) { (m, x) =>
      m.getOrElseUpdate(f(x), new ArrayBuffer[A]) += x
      m
    }.toSeq

  private[this] def duplicated[A](xs: collection.Seq[A]): collection.Seq[A] = xs.filter {
    val seen = new mutable.HashSet[A]
    x => !seen.add(x)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy