Cutting Boilerplate with GHC's DerivingVia: A Practical Look at Typeclass Composition
GHC's DerivingVia extension (since 8.6) lets you compose typeclass instances without boilerplate newtype wrappers. A practical walkthrough with JSON encoding example, role restrictions, and a verification checklist.
26 Feb 2026, 19:38 UTC

The Newtype Wrapper Tax
You have a newtype UserId = UserId Int and you want a Monoid instance that adds the underlying integers. The old way: write a newtype SumUserId = SumUserId UserId, derive Monoid via GeneralizedNewtypeDeriving, then wrap and unwrap everywhere. Or you manually implement mappend (UserId a) (UserId b) = UserId (a + b) and mempty = UserId 0. Both approaches add noise—either extra types or handwritten instances that mirror existing ones.
GHC 8.6 introduced DerivingVia to eliminate this tax. The extension lets you say "derive Monoid for UserId by delegating to Sum Int" in a single line. No wrapper types, no manual instances, no coerce calls in your code. The compiler handles the coercion proof and generates the instance.
How DerivingVia Works
The syntax is:
deriving via OtherType instance Class MyType
OtherType must have an existing Class instance, and MyType must be representationally equal to OtherType—meaning they have the same runtime representation and the last type parameter of Class has a representational role. GHC checks this at compile time and rejects mismatches with a clear role error.
Under the hood, GHC generates an instance that uses coerce from Data.Coerce to convert between the two types, applies the OtherType instance, and coerces back. You can see this with -ddump-deriv.
Worked Example: JSON Encoding via a Different Representation
Suppose you have a domain type that stores a timestamp as microseconds since epoch, but your API expects ISO 8601 strings. You could write custom ToJSON/FromJSON instances, or you can derive them via a newtype that already has the right encoding:
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Event where
import Data.Aeson (ToJSON, FromJSON)
import Data.Time (UTCTime)
import Data.Time.Format.ISO8601 (iso8601Show, iso8601ParseM)
import qualified Data.Text as T
newtype Microseconds = Microseconds Int64
deriving (Eq, Show)
deriving via Int64 instance ToJSON Microseconds
deriving via Int64 instance FromJSON Microseconds
newtype EventTime = EventTime UTCTime
deriving (Eq, Show)
deriving via Microseconds instance ToJSON EventTime
deriving via Microseconds instance FromJSON EventTime
EventTime gets JSON instances that serialize as integers (microseconds) without you writing any encoding logic. The Microseconds newtype serves as the via type—it has the ToJSON/FromJSON instances you want (inherited from Int64), and EventTime is representationally equal to Microseconds because both wrap a single Int64 field.
To verify the generated instances, compile with -ddump-deriv and look for coerce calls in the output.
Where It Falls Short
DerivingVia isn't a universal hammer. Three practical limits bite regularly:
- Role restrictions. The class's last parameter must have representational role.
Typeable,Data, and many GADT-based classes use nominal roles, soderiving viafails with a role error. You'll see this when trying to deriveTypeablevia another type. - Debugging obscurity. When multiple
viapaths exist for the same class, it's not obvious which instance GHC picked.:infoin GHCi shows the derived instance but not theviachain. In large codebases, this can make instance resolution surprising. - Invariant preservation. If the
viatype's instance relies on invariants (e.g., a smart constructor enforcing positive values), coercion bypasses those checks. DerivingMonoidfor aPositiveIntviaSum Intwould allow negative results at runtime.
When to Reach for DerivingVia
Use it when:
- You have a newtype wrapper around a type that already has the instance you want.
- The class parameter has representational role (most standard classes:
Eq,Ord,Monoid,ToJSON,Arbitrary, etc.). - The coercion preserves the semantic invariants you care about.
Skip it when the instance depends on hidden invariants, when the class uses nominal roles, or when the via type isn't in scope and adding it would create a confusing dependency.
Quick Verification Checklist
- Add
{-# LANGUAGE DerivingVia #-}to the module. - Write the deriving clause:
deriving via Sum Int instance Monoid Age. - Load in GHCi and test:
mappend (Age 3) (Age 5)should yieldAge 8. - Compile with
-ddump-derivto confirmcoerceappears in the generated instance. - Try a negative case (e.g.,
deriving via Int instance Eq (MyGADT a)with a nominal parameter) to see the role error.
DerivingVia doesn't replace good instance design—it just removes the mechanical overhead when the design is already sound. The next time you reach for a wrapper newtype solely to borrow an instance, try via first.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.