Repository: avro-kotlin/avro4k (kotlin)

Issue:
Default value for lists
Hi

Upon using @AvroDefault annotation, I noticed that there is no option to set a default empty list value.

```kotlin
when (fieldDescriptor.kind) {
               PrimitiveKind.INT -> it.toInt()
               PrimitiveKind.LONG -> it.toLong()
               PrimitiveKind.FLOAT -> it.toFloat()
               PrimitiveKind.BOOLEAN -> it.toBoolean()
               PrimitiveKind.BYTE -> it.toByte()
               PrimitiveKind.SHORT -> it.toShort()
               PrimitiveKind.STRING -> it
               else -> throw IllegalArgumentException("Cannot use a default value for type ${fieldDescriptor.kind}")
            }
```

Could StructureKind.LIST be supported so that the default value could be an empty [] array.

See  [Avro default value spec](https://avro.apache.org/docs/current/spec.html)

Thank you

Pull request:
Provide:
- null default for arrays
- empty list default for arrays
- primitive default for arrays

example:
```kotlin
@Serializable
data class BarArray(
   @AvroDefault(Avro.EMPTY_LIST)
   val defaultEmptyArray: List<String>,
   @AvroDefault(Avro.NULL)
   val nullableDefaultEmptyArray: List<String>?,
   @AvroDefault("1,2")
   val defaultStringArrayWith2Defaults: List<String>,
   @AvroDefault("1,2")
   val defaultIntArrayWith2Defaults: List<Int>,
   @AvroDefault("3.14, 9.89")
   val defaultFloatArrayWith2Defaults: List<Float>
)
```

Fixes #52

Patch:
diff --git a/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/Avro.kt b/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/Avro.kt
index 7e6bd989..bf9de2bd 100644
--- a/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/Avro.kt
+++ b/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/Avro.kt
@@ -90,6 +90,7 @@ class Avro(override val context: SerialModule = EmptyModule) : SerialFormat, Bin
          UUID::class to UUIDSerializer())
       )
       val default = Avro(simpleModule)
+
       /**
        * Use this constant if you want to explicitly set a default value of a field to avro null
        */
diff --git a/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/SerialDescriptor.kt b/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/SerialDescriptor.kt
index ee257f80..32da551f 100644
--- a/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/SerialDescriptor.kt
+++ b/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/SerialDescriptor.kt
@@ -1,9 +1,9 @@
 package com.sksamuel.avro4k
 
-import kotlinx.serialization.PolymorphicKind
-import kotlinx.serialization.SerialDescriptor
-import kotlinx.serialization.UnionKind
-import kotlinx.serialization.elementDescriptors
+import kotlinx.serialization.*
+import kotlinx.serialization.builtins.list
+import kotlinx.serialization.builtins.nullable
+import kotlinx.serialization.builtins.serializer
 
 fun SerialDescriptor.leafsOfSealedClasses() : List<SerialDescriptor> {
    return if (this.kind == PolymorphicKind.SEALED) {
@@ -11,4 +11,25 @@ fun SerialDescriptor.leafsOfSealedClasses() : List<SerialDescriptor> {
    } else {
       listOf(this)
    }
+}
+
+fun SerialDescriptor.serializer() : KSerializer<out Any?> {
+   val notNullableSerializer = when(kind){
+      PrimitiveKind.BOOLEAN -> Boolean.serializer()
+      PrimitiveKind.INT -> Int.serializer()
+      PrimitiveKind.LONG -> Long.serializer()
+      PrimitiveKind.FLOAT -> Float.serializer()
+      PrimitiveKind.BOOLEAN -> Boolean.serializer()
+      PrimitiveKind.BYTE -> Byte.serializer()
+      PrimitiveKind.SHORT -> Short.serializer()
+      PrimitiveKind.STRING -> String.serializer()
+      StructureKind.LIST -> this.elementDescriptors().single().serializer().list
+      else -> TODO("only implemented primitive types for now")
+}
+   return if(this.isNullable){
+      notNullableSerializer.nullable
+   }else{
+      notNullableSerializer
+   }
+
 }
\ No newline at end of file
diff --git a/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/schema/ClassSchemaFor.kt b/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/schema/ClassSchemaFor.kt
index 7fa7a0fd..d3d2da6e 100644
--- a/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/schema/ClassSchemaFor.kt
+++ b/avro4k-core/src/main/kotlin/com/sksamuel/avro4k/schema/ClassSchemaFor.kt
@@ -1,12 +1,14 @@
 package com.sksamuel.avro4k.schema
 
-import com.sksamuel.avro4k.AnnotationExtractor
-import com.sksamuel.avro4k.Avro
-import com.sksamuel.avro4k.AvroProp
-import com.sksamuel.avro4k.RecordNaming
+import com.sksamuel.avro4k.*
 import kotlinx.serialization.PrimitiveKind
 import kotlinx.serialization.SerialDescriptor
+import kotlinx.serialization.StructureKind
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonConfiguration
+import kotlinx.serialization.json.JsonDecodingException
 import kotlinx.serialization.modules.SerialModule
+import org.apache.avro.JsonProperties
 import org.apache.avro.Schema
 import org.apache.avro.SchemaBuilder
 
@@ -16,6 +18,9 @@ class ClassSchemaFor(private val descriptor: SerialDescriptor,
 
    private val entityAnnotations = AnnotationExtractor(descriptor.annotations)
    private val naming = RecordNaming(descriptor)
+   private val json by lazy {
+      Json(JsonConfiguration.Stable, context)
+   }
 
    override fun schema(): Schema {
       // if the class is annotated with @AvroValueType then we need to encode the single field
@@ -83,10 +88,11 @@ class ClassSchemaFor(private val descriptor: SerialDescriptor,
          else -> schemaOrFixed.overrideNamespace(ns)
       }
 
+
       val default: Any? = annos.default()?.let {
-         if(it == Avro.NULL){
+         if (it == Avro.NULL) {
             Schema.Field.NULL_DEFAULT_VALUE
-         }else {
+         } else {
             when (fieldDescriptor.kind) {
                PrimitiveKind.INT -> it.toInt()
                PrimitiveKind.LONG -> it.toLong()
@@ -95,6 +101,10 @@ class ClassSchemaFor(private val descriptor: SerialDescriptor,
                PrimitiveKind.BYTE -> it.toByte()
                PrimitiveKind.SHORT -> it.toShort()
                PrimitiveKind.STRING -> it
+               StructureKind.LIST ->
+                  decodeJsonDefaultAsList(fieldDescriptor
+                  , it)
+
                else -> throw IllegalArgumentException("Cannot use a default value for type ${fieldDescriptor.kind}")
             }
          }
@@ -107,4 +117,15 @@ class ClassSchemaFor(private val descriptor: SerialDescriptor,
 
       return field
    }
-}
\ No newline at end of file
+
+   private fun  decodeJsonDefaultAsList(listFieldDescriptor: SerialDescriptor, jsonString: String): List<Any> = try {
+      // the list entries will be parsed according to their kind
+      val decodedValue = json.parse(listFieldDescriptor.serializer(),jsonString)
+      (decodedValue as? List<*>)?.map { it?:JsonProperties.NULL_VALUE } ?: error("Serializer of an array field descriptor did not return a List in its deserialized form.")
+   } catch (jde: JsonDecodingException) {
+      throw IllegalArgumentException("Cannot use default value $jsonString. ${jde.message}",jde)
+   }
+
+
+}
+


Test patch:
diff --git a/avro4k-core/src/test/kotlin/com/sksamuel/avro4k/schema/AvroDefaultSchemaTest.kt b/avro4k-core/src/test/kotlin/com/sksamuel/avro4k/schema/AvroDefaultSchemaTest.kt
index 1288f96f..14131a00 100644
--- a/avro4k-core/src/test/kotlin/com/sksamuel/avro4k/schema/AvroDefaultSchemaTest.kt
+++ b/avro4k-core/src/test/kotlin/com/sksamuel/avro4k/schema/AvroDefaultSchemaTest.kt
@@ -3,10 +3,11 @@ package com.sksamuel.avro4k.schema
 import com.sksamuel.avro4k.Avro
 import com.sksamuel.avro4k.AvroDefault
 import io.kotlintest.shouldBe
+import io.kotlintest.shouldThrow
 import io.kotlintest.specs.FunSpec
 import kotlinx.serialization.Serializable
 
-class AvroDefaultSchemaTest: FunSpec() {
+class AvroDefaultSchemaTest : FunSpec() {
    init {
       test("schema for data class with @AvroDefault should include default value as a string") {
          val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_string.json"))
@@ -25,6 +26,30 @@ class AvroDefaultSchemaTest: FunSpec() {
          val schema = Avro.default.schema(BarFloat.serializer())
          schema.toString(true) shouldBe expected.toString(true)
       }
+
+      test("schema for data class with @AvroDefault should include default value as a list") {
+         val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_list.json"))
+         val schema = Avro.default.schema(BarList.serializer())
+         schema.toString(true) shouldBe expected.toString(true)
+      }
+
+      test("schema for data class with @AvroDefault should include default value as an array") {
+         val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_array.json"))
+         val schema = Avro.default.schema(BarArray.serializer())
+         schema.toString(true) shouldBe expected.toString(true)
+      }
+
+      test("schema for data class with @AvroDefault should include default value as an set") {
+         val expected = org.apache.avro.Schema.Parser().parse(javaClass.getResourceAsStream("/avro_default_annotation_set.json"))
+         val schema = Avro.default.schema(BarSet.serializer())
+         schema.toString(true) shouldBe expected.toString(true)
+      }
+
+      test("schema for data class with @AvroDefault should throw error when array type does not match default value type") {
+         shouldThrow<IllegalArgumentException> { Avro.default.schema(BarInvalidArrayType.serializer()) }
+         shouldThrow<NotImplementedError> { Avro.default.toRecord(BarInvalidNonPrimitiveType.serializer(), BarInvalidNonPrimitiveType()) }
+         shouldThrow<IllegalArgumentException> { Avro.default.toRecord(BarInvalidNonArrayType.serializer(), BarInvalidNonArrayType()) }
+      }
    }
 }
 
@@ -35,9 +60,9 @@ data class BarString(
    @AvroDefault("hello")
    val b: String,
    @AvroDefault(Avro.NULL)
-   val nullableString : String?,
+   val nullableString: String?,
    @AvroDefault("hello")
-   val c:String?
+   val c: String?
 )
 
 @Serializable
@@ -46,9 +71,9 @@ data class BarInt(
    @AvroDefault("5")
    val b: Int,
    @AvroDefault(Avro.NULL)
-   val nullableInt : Int?,
+   val nullableInt: Int?,
    @AvroDefault("5")
-   val c:Int?
+   val c: Int?
 )
 
 @Serializable
@@ -57,8 +82,87 @@ data class BarFloat(
    @AvroDefault("3.14")
    val b: Float,
    @AvroDefault(Avro.NULL)
-   val nullableFloat : Float?,
+   val nullableFloat: Float?,
    @AvroDefault("3.14")
-   val c:Float?
+   val c: Float?
+)
+
+@Serializable
+data class BarSet(
+   @AvroDefault("[]")
+   val defaultEmptySet: Set<String>,
+   @AvroDefault(Avro.NULL)
+   val nullableDefaultEmptySet: Set<String>?,
+   @AvroDefault("""["John", "Doe"]""")
+   val defaultStringSetWith2Defaults: Set<String>,
+   @AvroDefault("""[1, 2]""")
+   val defaultIntSetWith2Defaults: Set<Int>,
+   @AvroDefault("""[3.14, 9.89]""")
+   val defaultFloatSetWith2Defaults: Set<Float>,
+   //Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in
+   //the default
+   //See https://issues.apache.org/jira/browse/AVRO-2647
+   @AvroDefault("""[null]""")
+   val defaultStringSetWithNullableTypes : Set<String?>
+)
+@Serializable
+data class BarList(
+   @AvroDefault("[]")
+   val defaultEmptyList: List<String>,
+   @AvroDefault(Avro.NULL)
+   val nullableDefaultEmptyList: List<String>?,
+   @AvroDefault("""["John", "Doe"]""")
+   val defaultStringListWith2Defaults: List<String>,
+   @AvroDefault("""[1, 2]""")
+   val defaultIntListWith2Defaults: List<Int>,
+   @AvroDefault("""[3.14, 9.89]""")
+   val defaultFloatListWith2Defaults: List<Float>,
+   //Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in
+   //the default
+   //See https://issues.apache.org/jira/browse/AVRO-2647
+   @AvroDefault("""[null]""")
+   val defaultStringListWithNullableTypes : List<String?>
+)
+@Suppress("ArrayInDataClass")
+@Serializable
+data class BarArray(
+   @AvroDefault("[]")
+   val defaultEmptyArray: Array<String>,
+   @AvroDefault(Avro.NULL)
+   val nullableDefaultEmptyArray: Array<String>?,
+   @AvroDefault("""["John", "Doe"]""")
+   val defaultStringArrayWith2Defaults: Array<String>,
+   @AvroDefault("""[1, 2]""")
+   val defaultIntArrayWith2Defaults: Array<Int>,
+   @AvroDefault("""[3.14, 9.89]""")
+   val defaultFloatArrayWith2Defaults: Array<Float>,
+   //Unions are currently not correctly supported by Java-Avro, so for now we do not test with null values in
+   //the default
+   //See https://issues.apache.org/jira/browse/AVRO-2647
+   @AvroDefault("""[null]""")
+   val defaultStringArrayWithNullableTypes : Array<String?>
+)
 
+@Serializable
+data class BarInvalidArrayType(
+   @com.sksamuel.avro4k.AvroDefault("""["foo-bar"]""")
+   val defaultFloatArrayWith2Defaults: List<Float>
 )
+
+@Serializable
+class FooBar
+
+@Serializable
+data class BarInvalidNonPrimitiveType(
+   @AvroDefault("test-value")
+   val defaultBarArrayWithNonWorkingDefaults: List<FooBar> = ArrayList()
+)
+
+@Serializable
+data class BarInvalidNonArrayType(
+   @AvroDefault("{}")
+   val defaultBarArrayWithNonWorkingDefaults: List<Int> = ArrayList()
+)
+
+
+
diff --git a/avro4k-core/src/test/resources/avro_default_annotation_array.json b/avro4k-core/src/test/resources/avro_default_annotation_array.json
new file mode 100644
index 00000000..3308531a
--- /dev/null
+++ b/avro4k-core/src/test/resources/avro_default_annotation_array.json
@@ -0,0 +1,58 @@
+{
+   "type": "record",
+   "name": "BarArray",
+   "namespace": "com.sksamuel.avro4k.schema",
+   "fields": [
+      {
+         "name": "defaultEmptyArray",
+         "type": {
+            "type": "array",
+            "items": "string"
+         },
+         "default": []
+      },
+      {
+         "name": "nullableDefaultEmptyArray",
+         "type": [
+            "null",
+            {
+               "type": "array",
+               "items": "string"
+            }
+         ],
+         "default": null
+      },
+      {
+         "name": "defaultStringArrayWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "string"
+         },
+         "default": ["John", "Doe"]
+      },
+      {
+         "name": "defaultIntArrayWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "int"
+         },
+         "default": [1,2]
+      },
+      {
+         "name": "defaultFloatArrayWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "float"
+         },
+         "default": [3.14, 9.89]
+      },
+      {
+         "name": "defaultStringArrayWithNullableTypes",
+         "type": {
+            "type": "array",
+            "items": [ "null", "string" ]
+         },
+         "default" : [ null ]
+      }
+   ]
+}
diff --git a/avro4k-core/src/test/resources/avro_default_annotation_list.json b/avro4k-core/src/test/resources/avro_default_annotation_list.json
new file mode 100644
index 00000000..ce59bdab
--- /dev/null
+++ b/avro4k-core/src/test/resources/avro_default_annotation_list.json
@@ -0,0 +1,58 @@
+{
+   "type": "record",
+   "name": "BarList",
+   "namespace": "com.sksamuel.avro4k.schema",
+   "fields": [
+      {
+         "name": "defaultEmptyList",
+         "type": {
+            "type": "array",
+            "items": "string"
+         },
+         "default": []
+      },
+      {
+         "name": "nullableDefaultEmptyList",
+         "type": [
+            "null",
+            {
+               "type": "array",
+               "items": "string"
+            }
+         ],
+         "default": null
+      },
+      {
+         "name": "defaultStringListWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "string"
+         },
+         "default": ["John", "Doe"]
+      },
+      {
+         "name": "defaultIntListWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "int"
+         },
+         "default": [1,2]
+      },
+      {
+         "name": "defaultFloatListWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "float"
+         },
+         "default": [3.14, 9.89]
+      },
+      {
+         "name": "defaultStringListWithNullableTypes",
+         "type": {
+            "type": "array",
+            "items": [ "null", "string" ]
+         },
+         "default" : [ null ]
+      }
+   ]
+}
diff --git a/avro4k-core/src/test/resources/avro_default_annotation_set.json b/avro4k-core/src/test/resources/avro_default_annotation_set.json
new file mode 100644
index 00000000..c42616ec
--- /dev/null
+++ b/avro4k-core/src/test/resources/avro_default_annotation_set.json
@@ -0,0 +1,58 @@
+{
+   "type": "record",
+   "name": "BarSet",
+   "namespace": "com.sksamuel.avro4k.schema",
+   "fields": [
+      {
+         "name": "defaultEmptySet",
+         "type": {
+            "type": "array",
+            "items": "string"
+         },
+         "default": []
+      },
+      {
+         "name": "nullableDefaultEmptySet",
+         "type": [
+            "null",
+            {
+               "type": "array",
+               "items": "string"
+            }
+         ],
+         "default": null
+      },
+      {
+         "name": "defaultStringSetWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "string"
+         },
+         "default": ["John", "Doe"]
+      },
+      {
+         "name": "defaultIntSetWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "int"
+         },
+         "default": [1,2]
+      },
+      {
+         "name": "defaultFloatSetWith2Defaults",
+         "type": {
+            "type": "array",
+            "items": "float"
+         },
+         "default": [3.14, 9.89]
+      },
+      {
+         "name": "defaultStringSetWithNullableTypes",
+         "type": {
+            "type": "array",
+            "items": [ "null", "string" ]
+         },
+         "default" : [ null ]
+      }
+   ]
+}