Marten 0.7.0 release notes
August 29, 2026.
Requirements and compatibility
- Crystal: 1.17, 1.18, 1.19, 1.20, and 1.21.
- Databases:
- MariaDB 10.11 and higher.
- MySQL 8.4 and higher.
- PostgreSQL 15 and higher.
- SQLite 3.37.0 and higher.
New features
Polymorphic relationships
Polymorphic relationships can now be defined through the use of polymorphic fields. Those are useful when you want to store a reference to a record whose model can vary among a predefined set of possible types.
For example:
class Article < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
end
class Recipe < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 128
end
class Comment < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :target, :polymorphic, to: [Article, Recipe], related: :comments, on_delete: :cascade
field :text, :text
end
In the above example, a Comment record could be associated with an Article or a Recipe record, and each of these models could have many associated Comment records. The on_delete option can be used to configure how related records are handled when a targeted record is deleted, like for other relation fields.
Helper methods and convenient scopes are also generated for polymorphic fields, allowing to interact with the related records in a type-safe manner. For example:
# Create an article
article = Article.create!(title: "This is an article")
# Create a recipe
recipe = Recipe.create!(title: "This is a recipe")
# Create a comment
comment = Comment.create!(text: "This is a comment", target: article)
# Regular getter methods
comment.target # => #<Article:0x1036e3ee0 id: 1, title: "This is an article">
comment.target_type # => "Article"
comment.target_id # => 1
# Type class getter method
comment.target_class # => Article (or nil if no related record is set)
comment.target_class! # => Article (or raise if no related record is set)
# Predicate helper methods
comment.article_target? # => true
comment.recipe_target? # => false
# Typed getters methods
comment.article_target # => Returns the associated Recipe record if the targeted record is indeed a Recipe record (or nil otherwise)
comment.article_target! # => Returns the associated Recipe record if the targeted record is indeed a Recipe record (or raise otherwise)
# Type-specific model scopes (generated based on the specified type classes)
Comment.with_article_target # => Returns all the comments associated with Article records
Comment.with_recipe_target # => Returns all the comments associated with Recipe records
Please refer to Polymorphic relationships to learn more about this new capability.
Email attachments
Marten emails can now include attachments through a dedicated #attach method, making it easy to attach files from disk, existing File objects, or arbitrary IOs.
For example:
class WelcomeEmail < Marten::Email
to @user.email
subject "Hello!"
template_name "emails/welcome_email.html"
before_deliver :add_attachments
def initialize(@user : User)
end
private def add_attachments
# Attach a file from a path.
attach "public/docs/terms.pdf"
# Attach generated content from an IO.
attach generate_pdf_io(@user), filename: "welcome.pdf", mime_type: "application/pdf"
end
end
Please refer to the Defining attachments section to learn more about this feature.
Field transformations
Query sets can now filter on date and date/time components through field transformations. These lookups apply the extraction at the SQL level (for example with strftime on SQLite or EXTRACT on PostgreSQL) instead of loading and comparing values in Crystal.
The following transformations are available:
year,month, andday— fordate_timeanddatefieldshour,minute, andsecond— fordate_timefields only
They can be combined with the usual field predicates. For example:
Article.filter(released_on__year: 2022)
Article.filter(created_at__year__gte: 2022)
Article.filter(created_at__hour__lt: 12)
Article.filter(created_at__month__in: [11, 12])
Article.filter(created_at__year__isnull: false)
Please refer to the field transformations reference for the full list of supported lookups.
Decimal fields
It is now possible to define decimal model fields in order to persist fixed-precision decimal numbers. These fields map to BigDecimal values in Crystal and are the right choice for monetary amounts and other values that must not lose precision (unlike float fields).
Both max_digits and decimal_places are required:
class Product < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :price, :decimal, max_digits: 10, decimal_places: 2
end
product = Product.create!(price: BigDecimal.new("19.99"))
product.price # => 19.99
# Float, Int, and String values are also accepted
product.price = 19.99
decimal schema fields are also available for validating incoming form and API data as BigDecimal values, using the same max_digits and decimal_places constraints:
class ProductSchema < Marten::Schema
field :price, :decimal, max_digits: 10, decimal_places: 2
end
Please refer to the model fields reference and the schema fields reference to learn more about decimal fields.
Template whitespace control
The Marten templating language now supports whitespace control through the use of hyphens in tag, variable, and comment delimiters. Adding a hyphen (-) immediately after an opening delimiter or immediately before a closing delimiter allows to strip adjacent whitespace (spaces, tabs, and newlines) from the rendered output.
For example:
{%- if username == "John Doe" -%}
Wow, {{ username -}} , you have a long name!
{%- else -%}
Hello there!
{%- endif %}
The above template would render Wow, John Doe, you have a long name! when username is set to John Doe, and Hello there! otherwise — without the indentation and extra newlines from the template source.
Please refer to the Whitespace control section for more information.
Minor features
Models and databases
- Query sets now provide a
#limitmethod and an#offsetmethod. Those methods respectively allow to limit the number of records returned and to offset the records returned. They act as aliases for the#[]method with a range. - Indexes can now be created and dropped without locking the table for writes by passing
concurrently: truetoadd_indexandremove_index. Migrations that use this option must setatomic false. - The
#updatequery set method can now be called on model classes directly, thus allowing to update all the records of the considered model with the specified values. - It is now possible to update only specific columns without running validations or callbacks for a model record by using the
#update_columnsmethod. - It is now possible to easily create or update records in a single call by using the
#update_or_createquery set method. An#update_or_create!variant is also available for the same purpose, but it raises in case updated/created records are invalid. - Query sets can now be updated using raw SQL expressions through the
#updatemethod, allowing to perform SQL-level updates (eg. copying one column into another). - Query sets can now be filtered with enum values directly.
stringfields now support achoicesoption allowing to restrict field values to a predefined set of strings.- Improve thread-safety of database connection transaction tracking and database connection pool initialization, making it safer to run Marten applications with Crystal's multi-threading support.
Handlers and HTTP
Marten::HTTP::UploadedFileobjects can now be serialized to JSON.- A convenient
#remote_ip_addressmethod was added to theMarten::HTTP::Requestclass, allowing to easily retrieve the remote IP address of the incoming request. - A new
request_max_body_sizesetting was introduced, allowing to limit the size of incoming request bodies in order to mitigate denial-of-service attacks. Requests exceeding this limit (2.5MB by default) result in aMarten::HTTP::Errors::RequestBodyTooBigexception and a 400 Bad Request response. This protection can be disabled by settingrequest_max_body_sizetonil. - An X-Content-Type-Options middleware was introduced to automatically set the
X-Content-Type-Options: nosniffheader in responses, which prevents browsers from MIME-sniffing responses away from the declaredContent-Type. This header can be disabled on a per-handler basis by using the#exempt_from_x_content_type_optionsclass method. - A Cross-Origin-Opener-Policy middleware was introduced to automatically set the Cross-Origin-Opener-Policy header in responses. The header value is configurable via the
cross_origin_opener_policysetting (defaults tosame-origin). A custom value can be defined on a per-handler basis by using the#cross_origin_opener_policyclass method, and the header can be disabled on a per-handler basis by using the#exempt_from_cross_origin_opener_policyclass method.
Templates
- Most informational string properties (such as
#ascii_only?,#blank?,#bytesize,#empty?,#size, and#valid_encoding?) are now available as template attributes for string values and safe string values. - A
filtertemplate tag was introduced, allowing to apply one or more filters to the content of a template block. - A
truncatefilter was added, allowing to truncate a string so that it fits within a maximum number of characters. - Improve thread-safety of the cached template loader, making it safer to run Marten applications with Crystal's multi-threading support.
Schemas
stringfields now support achoicesoption allowing to restrict field values to a predefined set of strings.
Development
- The flash store can now be accessed from the spec client by using the
#flashmethod. See Flash messages for more details. - The ability to bind the server to a Unix socket was introduced by adding a new
socketsetting. - A new
main_app_labelsetting was introduced, allowing to configure the label of the main application. - The
migratemanagement command now accepts a--checkoption that exits with a non-zero status when unapplied migrations exist, without applying them. - The
migratemanagement command now accepts a--pruneoption that deletes nonexistent migrations from themarten_migrationstable. - The
genmigrationsmanagement command now accepts a--checkoption that exits with a non-zero status when model changes require migrations, without generating migration files. - The
genmigrationsmanagement command now accepts a--dry-runoption that shows migrations that would be generated without writing them. - The
genmigrationsmanagement command now accepts a--no-headeroption that does not add header comments at the top of newly generated migration files. - A new
collectartifactsmanagement command was introduced, allowing to collect runtime artifacts (eg. locales and templates) into a deployable directory that can be used with theroot_pathsetting in production (see Collecting runtime artifacts). - The
resetmigrationsmanagement command now accepts a--no-headeroption that does not add header comments at the top of newly generated migration files. - The
routesmanagement command now accepts a--grepoption that allows to only display routes whose path or name contains the given substring (case-insensitive). - A new
parallelismsetting was introduced, allowing to configure the maximum parallelism of Crystal's default execution context when starting the HTTP server (Crystal 1.21+). The initial value can also be provided via theMARTEN_PARALLELISMenvironment variable. - Projects generated with the
newmanagement command now include the X-Content-Type-Options middleware and the Cross-Origin-Opener-Policy middleware by default.
Caching
- Improve thread-safety of the
Marten::Cache::Store::Memorycache store, making it safer to run Marten applications with Crystal's multi-threading support.
Backward incompatible changes
Handlers and HTTP
- Incoming request bodies are now limited to 2.5MB by default through the
request_max_body_sizesetting. Requests exceeding this limit result in aMarten::HTTP::Errors::RequestBodyTooBigexception and a 400 Bad Request response. Applications that need to accept larger uploads (such as files) should increase this setting accordingly. This protection can also be disabled by settingrequest_max_body_sizetonil.