ch-robinson / dotnet-avro

An Avro implementation for .NET
https://engineering.chrobinson.com/dotnet-avro/
MIT License
135 stars 51 forks source link

Nullable fields from schema are not reflected in generated C# #169

Closed ievgenii-shepeliuk closed 2 years ago

ievgenii-shepeliuk commented 2 years ago

Hello

I'm using dotnet avro generate to create C# class from Avro schema.

My schema looks like

{
  "name": "Models.MyClass",
  "type": "record",
  "fields": [
    {"name": "BoolField", "type": ["null", "boolean"]},
    {"name": "StringField", "type": ["null", "string"]},
    {
      "name": "RecordField",
      "type": ["null", {
        "name": "Models.Credentials",
        "type": "record",
        "fields": [
          {"name": "Password", "type": "string"},
          {"name": "Username", "type": "string"}
        ]
      }]
    }, {
      "name": "EnumField",
      "type": ["null", {
        "name": "Models.AccountStatus",
        "type": "enum",
        "symbols": ["New", "Active"]
      }]
    }
  ]
}

I expect that properties BoolField, StringField, RecordField, EnumField will be nullable in generated class. But they are not. The resulting C# class looks like

namespace Models
{
    public enum AccountStatus
    {
        New,
        Active
    }

    public class Credentials
    {
        public string Password { get; set; }
        public string Username { get; set; }
    }

    public class MyClass
    {
        public bool? BoolField { get; set; }
        public string StringField { get; set; }
        public Credentials RecordField { get; set; }
        public AccountStatus EnumField { get; set; }
    }
}

Am I missing smth or this is a bug in code generation ?

dstelljes commented 2 years ago

At the moment, code generation only supports nullable value types (note the bool? above), not reference types. We plan to fully support nullable reference types in 8.x.

That said, enums are value types and thus should be supported currently; #171 fixes that bug and will be included in the next 7.2.x.

ievgenii-shepeliuk commented 2 years ago

Looking forward new release, thanks for fixing it.

ievgenii-shepeliuk commented 2 years ago

@dstelljes is it possible to somehow hack it on serialization level, so even if fields is not nullable in C# class - when value of reference type is null - it will be serialized as null ?

dstelljes commented 2 years ago

Yes, that’s how Chr.Avro currently behaves—the library is oblivious to C# 8+ nullable reference types.

ievgenii-shepeliuk commented 2 years ago

Can you provide an example ? I have an exception when serializing messages.

Although I have more complicated setup - i am wrapping Chr.Avro serializer to use it together with https://github.com/Farfetch/kafka-flow

I am creating an instance

_serializer = new AsyncSchemaRegistrySerializer<T>(schemaRegistryClient);

and then invoke SerializeAsync()

dstelljes commented 2 years ago

Sure, say I’m using your schema:

{
  "name": "Models.MyClass",
  "type": "record",
  "fields": [
    {
      "name": "RecordField",
      "type": ["null", {
        "name": "Models.Credentials",
        "type": "record",
        "fields": [
          {"name": "Password", "type": "string"},
          {"name": "Username", "type": "string"}
        ]
      }]
    }
  ]
}

and mapping it to these classes:

#nullable disable // turn off nullable reference types to match Chr.Avro

namespace Models
{
    public class Credentials
    {
        public string Password { get; set; }
        public string Username { get; set; }
    }

    public class MyClass
    {
        public Credentials RecordField { get; set; }
    }
}

the serializer would allow me to do this:

var context = new SerializationContext(MessageComponentType.Value, "example-topic");

// this should work because `RecordField` is `["null", ...]`
await _serializer.SerializeAsync(new MyClass { RecordField = null }, context);

but not this:

var context = new SerializationContext(MessageComponentType.Value, "example-topic");

// this would not work because `Password` is not `["null", ...]`
await _serializer.SerializeAsync(new MyClass { RecordField = new Credentials { Username = "user", Password = null } }, context);
ievgenii-shepeliuk commented 2 years ago

Yes, worked. Thanks for your help !

dstelljes commented 2 years ago

Full nullable reference type support is released with 8.0.0-pre.1.