Skip to content

Conversation

@timriley
Copy link
Member

@timriley timriley commented Oct 10, 2025

Provide new methods on the config.formats object that make it more flexible and easier to work with.

These are all new methods, intended to become the recommended usage of config.formats from Hanami 2.3 onwards.

For the purposes of compatibility, the existing methods remain in place, but will no longer be documented, and we will include notes on how to switch to the new methods in the upgrade guide. In a future release, these older methods will emit deprecation warnings, and will eventually be removed.

Fixes hanami/hanami#1396

Add formats.register, with types for distinct purposes

formats.register replaces formats.add. Its purpose is to register format to media type mappings only. Unlike .add, it does not set any of the given formats as being “accepted” by the action (see next section for the new way to do this).

This change makes it easier to register a range of formats e.g. in a base action class or in the app-level config.actions.formats, while ensuring you do not cause any inadvertent format restrictions in child action classes.

Example:

config.formats.register(:json, "application/json")

.register also allows you to register one or more media types for the distinct stages of request processing.

  • If you want to accept requests based on different/additional media types in Accept request headers, provide them as accept_types:
  • If you want to accept requests based on different/additional media types in Content-Type request headers, provide them as content_types:
  • If you do not provide these options, then the default media type (the required second argument, after the format name) is used for each of the above
  • This default media type is also set as the default Content-Type response header for requests that match the format

Example:

config.formats.register(
  :jsonapi,
  "application/vnd.api+json",
  accept_types: ["application/vnd.api+json", "application/json"],
  content_types: ["application/vnd.api+json", "application/json"],
)

Add formats.accepted and formats.accept

formats.accepted replaces formats.values. This name better reflects its purpose: it is the array of formats that the action will accept when handling requests.

To populate this array, you can still manually assign it, e.g. formats.accepted = [:json] as you could do previously with #values=.

However, the nicer (and recommended) way to do this is via config.accept, which takes a splat of format names. Example:

config.formats.accept :html, :json
config.formats.accepted # => [:html, :json]

config.formats.accept :csv # it is additive
config.formats.accepted # => [:html, :json, :csv]

This method is also a replacement for the higher-level config.format method that we currently have documented. In making these changes, I decided that it's better to teach users about the existence of the config.formats object and its own methods rather than trying to hide it away behind a single (and therefore inflexible) convenience method.

Add formats.default=

This is a brand new capability: you can now assign a default format via e.g. formats.default = :json.

The default format is used to set the default Content-Type response header if one of the configured accepted formats cannot be matched to the request's Accept header.

Previously, the default format was set to be the first of the formats that the config.formats becomes aware of, either via running config.formats.add or config.format or config.formats.values=. This was a bad arrangement because the setting of the default was implicit. It was not obvious from any of the methods being called and led to unexpected surprises. It could also not easily be overridden, which made things awkward in situations where action config is inherited from parent classes or app-level config.

Now we have a simple setter that allows the default format to be configured independently.

I should note that I have preserved the automatic setting of a default when calling formats.accept, but only if the formats.default is not already set with a non-nil value. This way we hopefully have the best of both worlds: a one-liner when someone wants it, but the ability to explicitly and separately configure the default format when that is required.

Next steps

I'm going to merge this and include this in 2.3.0.beta2, which will release soon.

In the meantime, I would also love to hear feedback from anyone who shared their experiences in #1396.

Perhaps separate to this PR, we should decide whether we want to make config.formats.accepted # => [:html] and config.formats.default # => :html the standard setup when inside full Hanami apps.

As a follow-up after merging this, I'll also begin an upgrade notes page in our guide to describe the changes above.

@cllns
Copy link
Member

cllns commented Oct 10, 2025

Sounds great to me! I love that we can add a new way of doing it without breaking existing API.

What do you think about potentially deprecating add immediately, instead of waiting for a future release?

@timriley
Copy link
Member Author

@cllns thanks for checking this out, glad you're happy with the direction.

I actually thought of deprecating immediately too! For a framework such as ours, I think we can take some liberties with moving a little faster (especially in this case where we have an easy mechanical change to replace to deprecated API), so with your encouragement I'll go ahead and just deprecate.

@afomera
Copy link
Member

afomera commented Oct 10, 2025

I think this should help clear up a LOT of confusion new users might feel 🙌! Great work on this. Can't wait to give this a try on my apps~.

Perhaps separate to this PR, we should decide whether we want to make config.formats.accepted # => [:html] and config.formats.default # => :html the standard setup when inside full Hanami apps.

Interesting, would the idea be to default apps apps to :json then? I'm curious (from a learning mechanism) how defaults like this get defined in Hanami-land. Do they just get coded into the methods, or would they be generated with the app's configuration?

I'll go ahead and just deprecate.

I think deprecating the previous, confusing behavior is a win too.

@timriley
Copy link
Member Author

Added the deprecation notices (using Ruby's built-in warn), expanded and updated tests, and I think this is good to go.

@timriley timriley marked this pull request as ready for review October 11, 2025 10:29
@timriley
Copy link
Member Author

Guides PR: hanami/guides#306

@timriley
Copy link
Member Author

And changes on Hanami's side: hanami/hanami#1544

@timriley
Copy link
Member Author

Just an update on this, I'm making another round of changes so that formats can be registered with separately configured media_types (checked in the Accept request header, and intended to be set in the Content-Type response header) versus content_types (checked in the Content-Type request header).

This should address the issue of the :html format needing a media_type of text/html but content_types of ["application/x-www-form-urlencoded", "multipart/form-data"].

I'll leave another update here once this is done.

@timriley timriley force-pushed the clarify-config-formats-api branch from 3d5100c to f632ce7 Compare October 16, 2025 12:19
@timriley
Copy link
Member Author

OK, I've made a few more changes to this to get the new behaviour working just so. I've updated the top-level PR description to reflect the latest changes, but I'll echo that here as well, for anyone following the PR:

Add formats.register, with types for distinct purposes

formats.register replaces formats.add. Its purpose is to register format to media type mappings only. Unlike .add, it does not set any of the given formats as being “accepted” by the action (see next section for the new way to do this).

This change makes it easier to register a range of formats e.g. in a base action class or in the app-level config.actions.formats, while ensuring you do not cause any inadvertent format restrictions in child action classes.

Example:

config.formats.register(:json, "application/json")

.register also allows you to register one or more media types for the distinct stages of request processing.

  • If you want to accept requests based on different/additional media types in Accept request headers, provide them as accept_types:
  • If you want to accept requests based on different/additional media types in Content-Type request headers, provide them as content_types:
  • If you do not provide these options, then the default media type (the required second argument, after the format name) is used for each of the above
  • This default media type is also set as the default Content-Type response header for requests that match the format

Example:

config.formats.register(
  :jsonapi,
  "application/vnd.api+json",
  accept_types: ["application/vnd.api+json", "application/json"],
  content_types: ["application/vnd.api+json", "application/json"],
)

I've also changed all references to "MIME types" to "media types", since that is the modern term for these. The only places I've left "MIME" in place is for our own MIME module (which is private API anyway) and any MIME-related methods we call or import from Rack.

@timriley timriley force-pushed the clarify-config-formats-api branch from 12b8cad to 0535c43 Compare October 17, 2025 03:18
@timriley timriley merged commit a1dec35 into main Oct 17, 2025
14 checks passed
@timriley timriley moved this to Done in Hanami 2.3 Oct 17, 2025
@timriley timriley deleted the clarify-config-formats-api branch October 17, 2025 03:43
timriley added a commit to hanami/guides that referenced this pull request Nov 8, 2025
This documents the behavioural change introduced in hanami/hanami-controller#485.
timriley added a commit to hanami/guides that referenced this pull request Nov 10, 2025
This documents the behavioural change introduced in hanami/hanami-controller#485.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Custom MIME types are not working

4 participants