The first thing that jumps out at me is the size of the type of your hash. You seem to be using Hash the same way as in Ruby. Don't.

In Ruby, or other dynamic languages, Hash es or objects are used as generic data containers, almost like unnamed classes. In Crystal, hashes have a single type for the key, and a single type for the value, which makes them unsuited for the task. You want to tell Crystal more about the structure of your data, so you don't have to keep repeating that.

The first thing to do is look at the result object. It can simply be transformed into a record Result :

record Result, characters: Array({Int64, String, String, Int16, Int8}), max_char_slots: Int32

the method then becomes

def handle_character_list(msg, client) sql = <<-SQL SELECT character_id, character_name, DATE_FORMAT(created, '%b:%d:%Y:%l:%i:%p') AS created, level, cc FROM rpg_characters WHERE user_id = ? SQL characters = db.query_all sql, client.user_id, as: {Int64, String, String, Int16, Int8} max_char_slots = client.user_data["max_char_slots"] Result.new(characters, max_char_slots) end

However, by looking at the method it might be that this Result record is only used in one place - to return data from this method. In that case it's unlikely you want to give it a more formal name. In this case you could use a NamedTuple . They're a bit like an anonymous record.

def handle_character_list(msg, client) sql = <<-SQL SELECT character_id, character_name, DATE_FORMAT(created, '%b:%d:%Y:%l:%i:%p') AS created, level, cc FROM rpg_characters WHERE user_id = ? SQL { characters: db.query_all(sql, client.user_id, as: {Int64, String, String, Int16, Int8}), max_char_slots: client.user_data["max_char_slots"] } end

Going further, we can see that a "Character" is also a type:

class Character getter id : Int64 getter name : String getter created : Time getter level : Int16 getter cc : Int8 def initialize(@id, @name, @created, @level, @cc) end end

We can then use the DB.mapping to define how the Character class looks in the database.

class Character DB.mapping({ id: Int64, name: String. created: Time, level: Int16, cc: Int8 }) def initialize(@id, @name, @created, @level, @cc) end end

This definition is equivalent to the previous one because DB.mapping automatically generates getters for us.

def handle_character_list(msg, client) sql = <<-SQL SELECT character_id, character_name, created, level, cc FROM rpg_characters WHERE user_id = ? SQL { characters: db.query_all(sql, client.user_id, as: Character), max_char_slots: client.user_data["max_char_slots"] } end

Going even further, I'd extract that into two methods, each one doing just one thing, and I'd probably make client.user_data more type safe:

def characters_for_user(user_id) sql = <<-SQL SELECT character_id, character_name, created, level, cc FROM rpg_characters WHERE user_id = ? SQL db.query_all(sql, user_id, as: Character) end def handle_character_list(msg, client) { characters: characters_for_user(client.user_id), max_character_slots: client.user_data.max_char_slots } end

This is just my thought process on how I'd write the code you've shown. I've made a lot of assumptions about your code and database which might be wrong (i.e. "created" is a DATETIME in mysql). I'm attempting to show a thought process, not a finished solution. Hope that helps.