salsify/avromatic

Nested Records are Not Instantiated

Opened this issue · 2 comments

When I define an Avromatic class, and instantiate it, I expect all the nested records to also be instantiated. Instead, only the top-most record is instantiated, and any sub-records are nil.

Here's a simple reproduction:

Avro Schema:

{
  "type": "record",
  "name": "top_rec",
  "namespace": "com.example",
  "fields": [
    {
      "name": "sub",
      "type": {
        "type": "record",
        "name": "sub_rec",
        "namespace": "com.example",
        "fields": [
          {
            "name": "i",
            "type": "int"
          }
        ]
      }
    }
  ]
}

Then we load the schema and create a class:

schema_file = File.read('top_rec.avsc')
schema = Avro::Schema.parse(schema_file)

nestedClass = Avromatic::Model.model(schema: schema)

myClass = nestedClass.new
puts myClass.inspect

Actual Result:

#<TopRec sub: nil>

Expected Result:

#<TopRec sub: #<SubRec i: nil>>

Obviously, it's possible to manually instantiate the entire class hierarchy, but this quickly becomes difficult as the level of nesting increases.

I am having the same issue, did you find a work around?

Sorry. We don't have a fix yet, mainly because we've moved to creating immutable models by default for performance reasons and this defaulting is likely to result in a model tree that's invalid and can't be made valid. I definitely see how it could be useful when dealing with mutable models though. In the meantime, you could workaround it via something like:

module ModelNestedRecordDefaulting
  EMPTY_HASH = {}.freeze

  def initialize(...)
    super

    attribute_definitions.each do |attribute_name, attribute_definition|
      if attribute_definition.type.is_a?(Avromatic::Model::Types::RecordType) && send(attribute_name).nil?
        send(attribute_definition.setter_name, EMPTY_HASH)
      end
    end
  end
end

# Include it in specific models
Avromatic::Model.model(schema: schema).include(ModelNestedRecordDefaulting)

# Alternatively patch it in globally for all models
module BuilderInclusionsPatch
  def inclusions
    super + [ModelNestedRecordDefaulting]
  end
end
Avromatic::Model::Builder.prepend(BuilderInclusionsPatch)