Skip to content

Add GADT typed actions with OpenAPI routes#2665

Draft
vcombey wants to merge 23 commits into
digitallyinduced:masterfrom
vcombey:feat/gadt-actions-openapi-routes
Draft

Add GADT typed actions with OpenAPI routes#2665
vcombey wants to merge 23 commits into
digitallyinduced:masterfrom
vcombey:feat/gadt-actions-openapi-routes

Conversation

@vcombey

@vcombey vcombey commented Apr 26, 2026

Copy link
Copy Markdown

Summary

  • add GADT typed action request/response specs with JSON/form body handling
  • derive OpenAPI route docs from the route DSL and typed action metadata
  • add routable Swagger UI/OpenAPI JSON controller actions configurable through [routes|...|]

Comment thread Guide/form.markdown
, body :: Text
, published :: Bool
}
deriving (Generic)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid Generic?

Generics are very slow on large records and break IHPs live reloading (because eventually it needs 10s for a reload)

With coding agents at least the boilerplate is quickly done :D

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hum, didn't know about that.
it breaks live reloading when editing the record you mean ?
Because I have 100 generic deriving types in my Application/Types and never saw a latency.
handwriten boilerplate can diverge though, could it be a macro that generates only the instance we need without the rest of generic ?
But for a json api it will also need FromJSON instance and ToSchema for the openapi..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only happens when a data structure has many fields or many constructors. See https://gitlab.haskell.org/ghc/ghc/-/issues/5642

I guess your 100 generic types are mostly small data structures (few fields, single constructor). For these cases it works. But in large IHP apps sometimes data structures have 30 fields or more (e.g. a users table might have 30 columns)

For the aeson JSON instances, can't we use template haskell?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should work for aeson yes,

What about ToSchema and form url encoding parsing ?
should we add some deriving TH, like https://github.com/ilyakooo0/deriving-openapi3/tree/master ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image I benchmarked it with a record with 100 field.

Comment thread Guide/form.markdown
<input ... value="Hello World" />
```

## Forms for Typed Actions

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a motivation why as a user from formFor I would want to use formForAction?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added more details, it is not entirerly clear for me what drift it prevent in details compared to formfor on a model.
But it adds the possibility to do typed form without a model.

@vcombey vcombey force-pushed the feat/gadt-actions-openapi-routes branch 2 times, most recently from 4bb83d7 to f81c083 Compare May 7, 2026 21:26
@vcombey vcombey marked this pull request as ready for review May 8, 2026 10:01
Copilot AI review requested due to automatic review settings May 8, 2026 10:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces an opt-in typed routing/controller layer (GADT indexed actions with typed request bodies and typed JSON responses) and builds OpenAPI/Swagger UI output directly from the route DSL + typed action/view metadata.

Changes:

  • Add typed action infrastructure (BodySpec/body decoding, typed-route dispatch, typed form helpers, method lookup, and response/status metadata).
  • Add OpenAPI generation + Swagger UI controller actions, including route-level metadata (summary, tags, operationId, success/response, private) and schema derivation.
  • Update routing DSL/parser and tests to support route annotations, plus package-qualified imports/wrappers around ihp-router modules.

Reviewed changes

Copilot reviewed 58 out of 58 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
UPGRADE.md Updates migration guidance for JSON rendering with the new typed JsonView response payload shape.
ihp/Test/Test/View/FormForSpec.hs Renames module to match filename and cleans up formatting.
ihp/Test/Test/Router/WebSocketSpec.hs Adjusts router trie import to disambiguate packages.
ihp/Test/Test/Router/TrieSpec.hs Uses package-qualified import for IHP.Router.Trie.
ihp/Test/Test/Router/MultiControllerSpec.hs Uses package-qualified imports for router DSL and capture.
ihp/Test/Test/Router/MixedModeSpec.hs Uses package-qualified imports for router DSL and capture.
ihp/Test/Test/Router/MiddlewareSpec.hs Uses package-qualified imports for trie/middleware modules.
ihp/Test/Test/Router/DSLQuoterSpec.hs Uses package-qualified imports for router DSL and capture.
ihp/Test/Test/Router/DSLParserSpec.hs Extends parser tests for route metadata/annotations and new route AST shape.
ihp/Test/Test/Router/CaptureSpec.hs Uses package-qualified import for capture module.
ihp/Test/Test/Router/AppBindingSpec.hs Uses package-qualified imports for router DSL and capture.
ihp/Test/Test/OpenApiSupportSpec.hs Adds end-to-end tests for typed JSON rendering, OpenAPI generation, and Swagger UI routes.
ihp/Test/Test/Main.hs Wires new/renamed specs into the test runner.
ihp/Test/Test/Controller/TypedActionSpec.hs Adds tests for typed body decoding, typed routing, typed forms, and OpenAPI metadata.
ihp/Test/RouterBench.hs Uses package-qualified import for trie types.
ihp/IHP/ViewSupport.hs Evolves JsonView to return a typed payload via JsonResponse associated type.
ihp/IHP/ViewPrelude.hs Re-exports IHP.OpenApiSupport.
ihp/IHP/View/Types.hs Extends FormContext with enctype, stable field IDs, validation lookup, and default submit label.
ihp/IHP/View/Form/TypedAction.hs Adds compatibility re-export module for typed action form helpers.
ihp/IHP/View/Form/Select.hs Adds selectFieldOptions/radioFieldOptions for plain input records; adds (Text, Text) CanSelect instance.
ihp/IHP/View/Form/FormFor.hs Adds typed-action form rendering (formForAction*), form enctype, stable field IDs, and submit label wiring.
ihp/IHP/View/Form/Fields.hs Refactors field helpers to use FormContext’s ID/validation functions; updates checkbox handling.
ihp/IHP/View/Form.hs Re-exports IHP.View.Form.TypedAction.
ihp/IHP/TypedControllerPrelude.hs Adds a prelude for typed controllers that hides conflicting classic controller functions.
ihp/IHP/Server.hs Inserts request-context middleware into the standard middleware stack.
ihp/IHP/RouterPrelude.hs Re-exports typed route support and Swagger UI controller type.
ihp/IHP/Router/UrlGenerator.hs Adds wrapper module re-exporting ihp-router URL generator.
ihp/IHP/Router/Types.hs Removes legacy ControllerRoute definition (now centralized in RouterSupport).
ihp/IHP/Router/TypedRoute.hs Adds runtime support for typed GADT routing + OpenAPI render expectation validation hooks.
ihp/IHP/Router/Trie.hs Adds wrapper module re-exporting ihp-router trie.
ihp/IHP/Router/Middleware.hs Adds wrapper module re-exporting ihp-router middleware.
ihp/IHP/Router/IHP.hs Extends TH route emitter to support typed GADT actions, metadata parsing, response/status handling, and OpenAPI docs emission.
ihp/IHP/Router/DSL/TH.hs Adds wrapper module re-exporting ihp-router TH utilities.
ihp/IHP/Router/DSL/Runtime.hs Adds wrapper module re-exporting ihp-router runtime DSL utilities.
ihp/IHP/Router/DSL/Parser.hs Adds wrapper module re-exporting ihp-router parser.
ihp/IHP/Router/DSL/AST.hs Adds wrapper module re-exporting ihp-router AST.
ihp/IHP/Router/Capture.hs Adds wrapper module re-exporting ihp-router capture logic.
ihp/IHP/OpenApiSupport.hs Adds OpenAPI schema derivation utilities, OpenAPI document generator, and Swagger UI controller/actions.
ihp/IHP/ErrorController.hs Adds request-context exception wrapping and richer 5xx request logging (including body decode summary).
ihp/IHP/ControllerSupport.hs Generalizes controller dispatch via ControllerAction + runControllerAction; wraps action/initContext in request-context handling.
ihp/IHP/ControllerPrelude.hs Exposes typed-action types/support in the prelude (while avoiding method name clashes).
ihp/IHP/Controller/TypedAction.hs Adds typed request body specs, decoding, request body OpenAPI docs, and the TypedController class.
ihp/IHP/Controller/Render.hs Introduces renderHtmlOrJsonWithStatusCode and validates typed OpenAPI render expectations on JSON responses.
ihp/IHP/AutoRefresh.hs Updates auto-refresh flow to use runControllerAction.
ihp/IHP/AuthSupport/Controller/Sessions.hs Removes redundant View import now covered by prelude exports.
ihp/ihp.cabal Adds new modules and dependencies for OpenAPI/typed actions and router wrappers.
ihp/default.nix Adds openapi3 (and hspec) to the Nix derivation inputs.
ihp-router/IHP/Router/DSL/TH.hs Extends TH metadata/reification to support GADTs, controller type vars, route annotations, and path template generation.
ihp-router/IHP/Router/DSL/Parser.hs Adds parsing for indented metadata lines and improves parse errors (e.g. missing HTTP method).
ihp-router/IHP/Router/DSL/AST.hs Extends route AST to include route annotations and updates documentation/comments.
ihp-ide/IHP/IDE/CodeGen/ApplicationGenerator.hs Adds guidance comment for generating OpenAPI docs from routes.
Guide/view.markdown Updates JSON rendering documentation to use renderHtmlOrJson and JsonView.
Guide/routing.markdown Adds documentation for typed GADT routes, metadata annotations, and OpenAPI integration.
Guide/json-api.markdown Documents typed request bodies and OpenAPI/Swagger UI usage.
Guide/form.markdown Documents formForAction for typed actions and typed form encoding/method behavior.
devenv-module.nix Adds openapi3 to the dev environment package set.
cabal.project Adds wai-early-return/ as a local package.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread UPGRADE.md
Comment on lines 23 to +28
-- After
instance View ShowView where
html ShowView { .. } = [hsx|...|]
type JsonResponse ShowView = Post

instance JsonView ShowView where
json ShowView { .. } = toJSON post
html ShowView { .. } = [hsx|...|]
json ShowView { .. } = post
> [routes|ControllerName
> GET /posts PostsAction
> POST /posts CreatePostAction
> /posts PostsAction
Comment thread ihp/ihp.cabal
Comment on lines 106 to 110
, wai-cors
, random
, openapi3
, hspec
, cereal
Comment thread ihp/IHP/ViewSupport.hs
Comment on lines +67 to 79
-- | A view that can also be rendered as JSON by 'renderHtmlOrJson'.
class JsonView theView where
json :: theView -> JSON.Value
type JsonResponse theView :: Type
type JsonResponse theView = JSON.Value

-- | Returns the typed JSON response payload for this view.
--
-- IHP renders JSON views by applying 'toJSON' to this value. This keeps
-- the rendered JSON and OpenAPI response schema tied to the same
-- 'JsonResponse' type.
json :: theView -> JsonResponse theView
json _ = error "Json View for this route is not implemented"

Comment on lines 18 to 22
import IHP.Router.DSL (routes)
import IHP.ControllerPrelude
import qualified IHP.WebSocket as WS
import qualified IHP.Router.Trie as Trie
import qualified "ihp-router" IHP.Router.Trie as Trie
import qualified Data.ByteString.Lazy as LBS
@vcombey vcombey marked this pull request as draft May 8, 2026 12:30
@vcombey vcombey force-pushed the feat/gadt-actions-openapi-routes branch from f81c083 to 12a1433 Compare May 8, 2026 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants