A collection of surprising, inconsistent, or quirky behaviors in Liquid that implementers should be aware of. These are documented here to help alternative implementations match the reference behavior, even when that behavior is counterintuitive.
Severity: Surprising
Discovered: Testing | size filter across types
int | size returns 8 (Ruby's byte representation size), while float | size returns 0 (because Float doesn't respond to .size).
{{ 42 | size }} => 8
{{ 3.14 | size }} => 0The size filter in Ruby Liquid calls input.respond_to?(:size) ? input.size : 0:
- Integer#size exists in Ruby and returns the byte representation size (8 bytes on 64-bit systems)
- Float#size does not exist in Ruby, so the filter returns
0
- All integers return
8regardless of their actual value (0,1,999999999all return8) - This is not the number of digits
- Implementers might expect
sizeto fail or returnnilfor numbers, not return arbitrary values
Using .size property access differs from the filter:
{{ 42.size }} => 8 (Integer has .size)
{{ 3.14.size }} => "" (Float has no .size, returns nil)But with the filter:
{{ 42 | size }} => 8
{{ 3.14 | size }} => 0 (filter returns 0, not empty)Severity: Inconsistent
Discovered: Testing first/last on hashes
hash | first returns the first key+value concatenated, but hash | last returns empty string.
{% assign h = "a" | split: "" %}
{{ h | first }} => Works for arrays
{% capture json %}{"a": 1, "b": 2}{% endcapture %}
{% assign h = json | parse_json %}
{{ h | first }} => "a1" (key + value concatenated)
{{ h | last }} => "" (empty!)Hashes in Ruby respond to first (returns [key, value]) but not to last in the same way. When rendered, the array [key, value] gets joined without a separator.
- Asymmetric behavior between
firstandlaston the same data structure - Hash's
firstoutput format is rarely useful (key and value mashed together) - Alternative implementations might expect
lastto work iffirstdoes
Severity: Inconsistent
Discovered: Comparing | filter vs .property behavior
The | size filter and .size property access behave differently for types without a native .size method.
| Type | x | size |
x.size |
|---|---|---|
| Array | 3 | 3 |
| Hash | 2 | 2 |
| String | 5 | 5 |
| Integer | 8 | 8 |
| Float | 0 | "" |
| Boolean | 0 | "" |
- Filter behavior: Returns
0whenrespond_to?(:size)is false - Property access: Returns
nil(rendered as empty) when method doesn't exist
- Cannot reliably use property access and filter interchangeably
- Implementers must handle both cases differently
0vs empty string can cause different behavior in conditionals
When you discover a new Liquid quirk, document it here with:
- Severity: How surprising/problematic is this?
- Example: Minimal reproduction showing the unexpected behavior
- Why This Happens: Technical explanation of the root cause
- Impact: What problems this causes for implementers/users