vulcan icon indicating copy to clipboard operation
vulcan copied to clipboard

Unable to set nonempty default for optional field

Open Kazark opened this issue 2 years ago • 7 comments

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.

Kazark avatar Apr 11 '23 20:04 Kazark

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.

Kazark avatar Apr 12 '23 18:04 Kazark

which looks like it will suffice for our case

Well, I hope. I have not fully validated this hope yet.

Kazark avatar Apr 12 '23 19:04 Kazark

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 avatar May 24 '23 14:05 soujiro32167

@soujiro32167 unless you have the null second, which is the way that Avro4s has it IIRC.

Kazark avatar May 24 '23 14:05 Kazark

Right, whatever the first is, is the one you can default to

soujiro32167 avatar May 24 '23 14:05 soujiro32167

Oh, Avro4s seems to change the way its option codecs work based on the default, sometimes putting null first, sometimes not.

Kazark avatar May 31 '23 19:05 Kazark

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.

Kazark avatar May 31 '23 20:05 Kazark