Search code examples
crystal-lang

How to initialize an object created by from_json in Crystal


Take a look at this example: https://play.crystal-lang.org/#/r/4hqb

Here's the code (don't know how long that link is good for...)

require "json"

class House
  JSON.mapping(address: String)

  getter first_part

  def initialize
    @first_part = address.split(" ")[0]
  end
end

house = House.from_json(%({"address": "Crystal Road 1234"))
puts house.first_part

It's the basics of the JSON.mapping example in the docs. Except I added an initialize which does something with the JSON data (address).

The trouble is we get this error:

Error in line 8: this 'initialize' doesn't explicitly initialize instance variable '@address' of House, rendering it nilable

Questions:

  • The default in mapping is 'nilable: false' and I don't want this to be nilable. Is there some way to expect this error to come from JSON which failed to specify address instead of making this a nilable field?
  • It would seem that there is another initialize somewhere that I should override (and call super on) but so far I haven't been successful. If that's the right approach, how is it done?

Solution

  • So far this is what I've come up with. I don't love it, but it achieves what I want. The two key things are:

    1. You have to use previous_def in an initialize with the right arg (PullParser type)
    2. You have to init the value to the type you want with the "catch-all initialization"

    Both are documented here: https://crystal-lang.org/docs/syntax_and_semantics/methods_and_instance_variables.html

    class House
      @first_part = ""
    
      JSON.mapping(
        address: {type: String, nilable: false}
      )
    
      getter first_part
    
      def initialize(pull : JSON::PullParser)
        previous_def
        @first_part = address.split(" ")[0]
      end
    
    end
    
    house = House.from_json(%({"address": "Crystal Road 1234"}))
    puts house.first_part
    

    Note: I say I don't love it because I feel like defining @first_part the way I do below guarantees it'll be a string and the previous_def call should just paste the code into the initializer defined by the JSON.mapping macro. Instead, because of the two definitions, we get an error that @first_part was not properly initialized in all of the initialize methods.