Skip to content
GitHub Repository Forum RSS-Newsfeed

Crystal 0.36.0 released!

Brian J. Cardiff

Crystal 0.36.0 has been released!

Since 0.35.1 there has been lots and lots of polishing, new features, and important fixes. This created a bigger delta than we wanted to transition from the last 0.x release to the first 1.0-preX. As a result, we are releasing a 0.36.0. This should help the community migrate their packages with less friction to the changes that will appear in 1.0. It will also give us a chance to get rid of a couple of recently introduced deprecations.

Note that this release will be available in the new apt and rpm repositories post, as well as GitHub release artifacts. You might need to update your package repositories.

There were 346 commits since 0.35.1 by 54 contributors.

Let’s get right into some highlights in this release. Don’t miss out on the rest of the release changelog.

Language changes

Although instance variables can have annotations, we must apply them in the base class that declares them. So we reject annotations on instance variables redefined in a child class. We might lift this restriction in the future, but we are going to play it safe first. Read more at #9502.

The ** operator is right-associative from now on. This matches Ruby and a couple of other programming languages. So 2 ** 2 ** 3 == 2 ** (2 ** 3) == 256. But note that the negation is done before the exponentiation because of how we parse negative number literals. So -2 ** 2 == (-2) ** 2 == 4, which is different to Ruby. Read more at #9684.

Some releases ago TypeNode#annotation was added. The former less powerful TypeNode#has_attribute? is deprecated now. Read more at #9950.


There are several fixes that, although formally lead to breaking-changes, are mostly edge cases that required some polishing.

  • You can’t use keywords as block arguments names. #9704
  • The a-b -c expression is now correctly parsed as (a - b) - c. #9652, #9884.

When using abstract defs, the compiler will enforce a couple of additional rules that should improve the maintenance of the code base.

  • Abstract def implementations must honor abstract declaration regarding type restrictions, splats, default values, and keyword arguments. #9585, #9634, #9633.
  • Abstract def implementations must honor return type abstract declaration. #9810

A simple example of what this enforce would be:

abstract class Foo
  abstract def m(x = 1)

class Bar < Foo
  def m(x) # Error: because Foo#m can be called with no argument.
                #           Declare it as def m(x = 1) to make the compiler happy.

Regarding typing, we improved a couple of stories.

  • The type variables unification is more correct regarding union types. (#10267, thanks @HertzDevil)
  • Exhaustive case expressions types to non-nilable value if all the cases allow it. #9659
  • Auto-casting deals better when the type restriction is a union by using, if possible, the exact type of the literal argument. #9610
  • Typing rules involving closured variables got smarter. #9986
  • Typing rules involving type restrictions, unions and boolean operators are really smarter. #10147.

How smarter you would like to know. Here is a sneak peak of what is now possible but it wasn’t before.

x = 1 || 1.0 || 'a' || "" # x : Int32 | Float64 | Char | String

# when-clauses of >= 3 types now work, including else-branches of the case statement
case x
when Int32, Float64, Char
  typeof(x) # => Int32 | Float64 | Char
  typeof(x) # => String

# negations of disjunctions now work
if !(x.is_a?(Int32) || x.is_a?(Float64))
  typeof(x) # => Char | String
  typeof(x) # => Int32 | Float64

# the de Morgan equivalent of the above also works now
if !x.is_a?(Int32) && !x.is_a?(Float64)
  typeof(x) # => Char | String
  typeof(x) # => Int32 | Float64

You can also declare defs overloads with different named tuple types. Really handy if you are into using tuples a lot. Read more at #10245.

A breaking-change that affects C bindings is introduced in #10254. Callbacks within lib should be declared now as alias instead of type. One less quirk in the compiler.

lib YourLib
---  type ACallback = Int32 -> Int32
+++ alias ACallback = Int32 -> Int32

A second breaking-change that affects C bindings is that a Crystal nil value is no longer translated to a null pointer value. Read more at #9872.

Standard library

The following top-level deprecated definitions are dropped: CRC32, Adler32, Flate, Gzip, Zip, Zlib, with_color. You will need to migrate to the Compress and Digest namespace. Read more at #9530, #9529, and #9531.

The SemanticVersion obeys the standard (as it should). This causes some breaking-changes if you rely on invalid version names, but otherwise you are safe. Read more at #9868.

Unfortunately some breaking-changes are silent. There is no simple way to show a warning. Such a case is when the return type of a method is changed. This is what happens to File.size and FileInfo#size since they now return Int64 instead of UInt64. Read more at #10015.


If you use Complex, you will need to rewrite some operations like Complex#exp, Complex#log, etc. to Math.exp, Math.log. This is the same API as for other numerical types. Read more at #9739.


String can hold arbitrary byte sequences. When these turned out to be invalid UTF-8 sequences, operations like String#index, String#includes?, and Char::Reader were misbehaving. In the presence of an invalid UTF-8 sequence, the data is now iterated a bit more slowly, like 1 byte at a time, until a valid sequence is found. Read more at #9713.


There is a set of changes seeking consistency over the collection types. Unfortunately some are silent breaking changes.

  • Hash#reject!, Hash#select!, and Hash#compact! return self always, as Array. Bonus point: you can method chain. #9904.
  • Set#delete returns Bool to indicate if the element was present. #9590.
  • Use Hash#reject! instead of Hash#delete_if. #9878.
  • Use Enumerable#select instead of Enumerable#grep. #9711.
  • Hash#key_index was removed. #10016

But not every change is a removal, the following additions are also part of the release!

  • Allow Iterator#zip to take multiple Iterator arguments. #9944
  • Array#shift and Array#unshift got faster. #10081


We try to have one way to do things. This comes at the expense of needing to migrate old solutions that served us well. JSON.mapping and YAML.mapping are no longer part of the std-lib since there is a more flexible alternative: JSON::Serializable, YAML::Serializable. You can either migrate or, if you still want them, you can use github:crystal-lang/ and github:crystal-lang/ Read more at #9527 and #9526.

You can now declare properties that will be used only on serialization or deserialization. If your model has a field that should be hashed for serialization this comes in really handy. Read more at #9567.

Another addition is that you can use use_json_discriminator and use_yaml_discriminator with other types than String. For example, you can use numbers or enums to map the corresponding type. Read more at #9222 and #10149.


It’s time to mention some small breaking-changes:

  • deprecated variants are no longer available. (#10051
  • Time::Span#duration is deprecated in favor of #abs. Time::Span is already a duration, right? #10144


Some breaking-changes are almost bug-fixes, but since they mean changing a known behavior, it is worth mentioning them as such. After #10180, FileUtils.cp_r will behave as expected when destination is a directory: the destination is calculated with File.join(dest_path, File.basename(src_path)).


The HTTP::Params is renamed to URI::Params. In this release there is a deprecated alias that will allow you to deal with this migration more gradually. But I would not count on this alias to remain for long. Read more at #10098.

Another breaking-change is the renaming of URI#full_path to URI#request_target. Some edge-case behaviors also change to match the expected definition. Read more at #10099.

URI::Params (former HTTP::Params), as you know, is used to represent query strings that can hold multiple values for a key. When assigning a new value to a key you will override all values of that key. Read more at #9605

params = URI::Params.parse("color=red&color=blue&console=gameboy")
params["color"] = "green"
params # => => "color=green&console=gameboy"

The HTTP::Client can now be used with any IO instead of TCPSocket. If you happen to have an application that uses HTTP as protocol over UNIX Sockets, like Docker, you can now talk to it. Read more at #9543

require "http"

io ="/var/run/docker.sock")
client =, "localhost")
response = client.get "/v1.40/images/json"


To have only one module to log them all, the former Logger is no longer in the std-lib. Migrate to Log or, for just a little longer, you can keep using github:crystal-lang/ Read more at #9525.

If you need more excuses to migrate to Log, the Log::Backend can now emit their entries with different strategies that should help keeping the throughput of the app in the presence of logging. Read more at #9432.


Time for some security alerts. The std-lib prevents by default the Secure Client-Initiated Renegotiation vulnerability attack. In #9815 you can find more info and a small patch if you need to secure existing apps. Also, if you use verify_mode=force-peer, it is now correctly set up. Read more at #9668.

The Digest module is now backed by OpenSSL. Most used algorithms have their own class that is easy to use: Digest::MD5, Digest::SHA1, Digest::SHA256, Digest::SHA512. Bonus: Digest#file and Digest#update(IO) are available to compute digest from file and IO content. Read more at #9864.


We hid some Channel API that should have been internal since the beginning. I doubt this will cause any issues. Read more #9564.

Channel#close now returns true unless the channel was already closed. This is handy in multi-thread, because you might need to know if the current fiber is the one that actually closed the channel. Read more at #9443.


Process.parse_arguments will allow you to parse a String in the same way a POSIX shell does. This is internally used to improve CRYSTAL_OPTS parsing. Read more at #9518.


The #should and #should_not methods can now take an additional argument for custom failure messages. Read more at #10127.

describe "something" do
  it "something" do
    true.should eq(false), "Oh no!"


The CI infrastructure for AArch64 hosted by works on arm is making progress. We are still not releasing official packages. Read more at #9508.

The doc generator got many improvements. In case you weren’t aware, besides generating HTML, it can also export the documentation information as JSON. This allows for the use of other documentation generation tools. With some of the recent additions listed in the changelog you can use mkdocs via some community development.

Next steps

Please update your Crystal and report any issues. We will keep moving forward and focusing on releasing 1.0.0-pre1 which should be a more stabilized version of 0.36 without many new additions.


We have been able to do all of this thanks to the continued support of 84codes, Nikola Motor Company and every other sponsor. It is extremely important for us to sustain the support through donations, so that we can maintain this development pace. OpenCollective is available for that.

Reach out to if you’d like to become a direct sponsor or find other ways to support Crystal. We thank you in advance!