JSON::Serializable => custom initialize

Hi.

Given this…

# serial_tester.cr

require "sdl-crystal-bindings/sdl3-crystal-bindings"
require "json"

class SerialTester
  include JSON::Serializable

  property width : UInt32
  property height : UInt32
  
  @[JSON::Field(ignore: true)]
  getter renderer : LibSDL::Renderer*

  @[JSON::Field(ignore: true)]
  getter pixels : Array(UInt32)

  def initialize(@width, @height, @renderer)
    size = @width * @height
    @pixels = Array(UInt32).new(size, 0x00000000)
  end
end
# app.cr

@renderer = LibSDL::create_renderer etc etc...
...
st = SerialTester.new(500, 500, @renderer)
j = st.to_json()
puts j

# outputs {"width":500,"height":500} as expected

and that :

# app.cr

@renderer = LibSDL::create_renderer etc etc...

st = SerialTester.new(500, 500, @renderer)
j = st.to_json()
st2 = SerialTester.from_json(j)

# compilation error
In /opt/homebrew/Cellar/crystal/1.16.3/share/crystal/src/json/serialization.cr:181:9

 181 | def initialize(*, __pull_for_json_serializable pull : ::JSON::PullParser)
           ^---------
Error: instance variable '@renderer' of SerialTester was not initialized in this 'initialize', rendering it nilable

I couldn’t find a lot of JSON::Serializable examples for how to deal with custom initialize methods and am wondering what is the standard way to work with that.

There is a #after_initialize method that gets called internally, if defined, after the instance is instantiated. However this won’t help you because there’d still be no way to set @renderer and keep it non-nilable. If you make it nilable, maybe using a non-public setter, you could use getter! to treat it as still not-nilable when you access it via self.renderer. Then a custom .from_json constructor to first call .from_json to get your instance, then #renderer= ... to set your renderer.

Another option would be to leverage the converter JSON::Field annotation to define how to provide the renderer from the JSON. Given it’s not actually in the JSON I think you could just not do anything with the pull parser and just return the renderer somehow. Tho this may not be straightforward either depending on where/how the renderer comes into existance.

Maybe the best option would be to have a separate service/type that handles the rendering and keep this type as more of a DTO so you dont have to deal with it at all.

Thanks. Clear.