Unable to set nonempty default for optional field
I am working on converting some contracts from using Avro4s to Vulcan. Because they are existing contracts I am constrained in what I can do---I am attempting to produce precisely equivalent Avro before and after.
I've hit a case that's giving me trouble. The following is a MWE.
import vulcan.*
final case class Foo(bar: Option[Int])
object Foo {
implicit lazy val codec: Codec[Foo] =
Codec.record(
name = "Foo",
namespace = "com.example",
)(field =>
field(
"bar",
_.bar,
default = Some(Some(1337)),
).map(Foo.apply)
)
}
This results in a Foo.codec.schema which is a Left containing
vulcan.AvroException$$anon$1: org.apache.avro.AvroTypeException: Invalid default for field bar: 1337 not a ["null","int"]
In my example, I was using an enum, not an integer, and got the same behavior. I noticed that Some(None) works as a default, but this does not match my existing contracts.
EDIT: the below is the original post, but a warning to those who find it hereafter that this did not work.
A coworker discovered a workaround which looks like it will suffice for our case: that is, everywhere we are using that enumeration, we are using the same default. Setting the default on the enumeration instead of on the field, works.
So we can probably move ahead; but the above still looks like a bug to me.
which looks like it will suffice for our case
Well, I hope. I have not fully validated this hope yet.
Got hit with this issue as well, and discovered an annoyance: https://avro.apache.org/docs/1.8.1/spec.html#Unions
(Note that when a default value is specified for a record field whose type is a union, the type of the default value must match the first element of the union. Thus, for unions containing "null", the "null" is usually listed first, since the default value of such unions is typically null.)
So the default value for an Option can only be None
@soujiro32167 unless you have the null second, which is the way that Avro4s has it IIRC.
Right, whatever the first is, is the one you can default to
Oh, Avro4s seems to change the way its option codecs work based on the default, sometimes putting null first, sometimes not.
This did not work. Looks like this will, however:
private implicit lazy val optionAllowingDefaultSome
: Codec[Option[Int]] =
Codec.union(builder =>
builder[Some[Int]] <+>
builder[None.type]
)
A locally-scoped monomorphic implicit that puts the null last, as Avro4s does.