diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## [1.2.0] – December 26, 2025
+
+- Add `Cretheus.Encode.tuple2`, up to length 9
+- Add `Cretheus.Decode.tuple2`, up to length 9
+
 ## [1.1.0] – October 24, 2025
 
 - Add `Cretheus.Decode.integer`
diff --git a/cretheus.cabal b/cretheus.cabal
--- a/cretheus.cabal
+++ b/cretheus.cabal
@@ -13,7 +13,7 @@
 stability: experimental
 synopsis: A clean aeson wrapper
 tested-with: GHC == 9.8.4, GHC == 9.10.1, GHC == 9.12.2
-version: 1.1.0
+version: 1.2.0
 
 extra-source-files:
   CHANGELOG.md
diff --git a/src/Cretheus/Decode.hs b/src/Cretheus/Decode.hs
--- a/src/Cretheus/Decode.hs
+++ b/src/Cretheus/Decode.hs
@@ -28,6 +28,8 @@
     array,
     vector,
     set,
+    tuple2,
+    tuple3,
 
     -- ** Object decoders
     ObjectDecoder,
@@ -45,6 +47,14 @@
 
     -- ** Decoder refinement
     refine,
+
+    -- ** Longer tuple decoders
+    tuple4,
+    tuple5,
+    tuple6,
+    tuple7,
+    tuple8,
+    tuple9,
   )
 where
 
diff --git a/src/Cretheus/Encode.hs b/src/Cretheus/Encode.hs
--- a/src/Cretheus/Encode.hs
+++ b/src/Cretheus/Encode.hs
@@ -28,6 +28,8 @@
     array,
     vector,
     set,
+    tuple2,
+    tuple3,
 
     -- ** Object encoders
     PropertyEncoding,
@@ -42,6 +44,14 @@
 
     -- ** Value encoders
     value,
+
+    -- ** Longer tuple encoders
+    tuple4,
+    tuple5,
+    tuple6,
+    tuple7,
+    tuple8,
+    tuple9,
   )
 where
 
diff --git a/src/Cretheus/Internal/Decode.hs b/src/Cretheus/Internal/Decode.hs
--- a/src/Cretheus/Internal/Decode.hs
+++ b/src/Cretheus/Internal/Decode.hs
@@ -24,6 +24,14 @@
     refine,
     set,
     text,
+    tuple2,
+    tuple3,
+    tuple4,
+    tuple5,
+    tuple6,
+    tuple7,
+    tuple8,
+    tuple9,
     utcTime,
     value,
     vector,
@@ -206,6 +214,192 @@
 set =
   fmap Set.fromList . list
 {-# INLINEABLE set #-}
+
+-- | A 2-tuple decoder.
+tuple2 :: (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c
+tuple2 f (Decoder fa) (Decoder fb) =
+  Decoder
+    ( Aeson.withArray "" \v ->
+        if Vector.length v == 2
+          then
+            f
+              <$> fa (Vector.unsafeIndex v 0)
+              <*> fb (Vector.unsafeIndex v 1)
+          else
+            fail ("expected 2-element array, but found " ++ show (Vector.length v))
+    )
+{-# INLINEABLE tuple2 #-}
+
+-- | A 3-tuple decoder.
+tuple3 :: (a -> b -> c -> d) -> Decoder a -> Decoder b -> Decoder c -> Decoder d
+tuple3 f (Decoder fa) (Decoder fb) (Decoder fc) =
+  Decoder
+    ( Aeson.withArray "" \v ->
+        if Vector.length v == 3
+          then
+            f
+              <$> fa (Vector.unsafeIndex v 0)
+              <*> fb (Vector.unsafeIndex v 1)
+              <*> fc (Vector.unsafeIndex v 2)
+          else
+            fail ("expected 3-element array, but found " ++ show (Vector.length v))
+    )
+{-# INLINEABLE tuple3 #-}
+
+-- | A 4-tuple decoder.
+tuple4 :: (a -> b -> c -> d -> e) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e
+tuple4 f (Decoder fa) (Decoder fb) (Decoder fc) (Decoder fd) =
+  Decoder
+    ( Aeson.withArray "" \v ->
+        if Vector.length v == 4
+          then
+            f
+              <$> fa (Vector.unsafeIndex v 0)
+              <*> fb (Vector.unsafeIndex v 1)
+              <*> fc (Vector.unsafeIndex v 2)
+              <*> fd (Vector.unsafeIndex v 3)
+          else
+            fail ("expected 4-element array, but found " ++ show (Vector.length v))
+    )
+{-# INLINEABLE tuple4 #-}
+
+-- | A 5-tuple decoder.
+tuple5 :: (a -> b -> c -> d -> e -> f) -> Decoder a -> Decoder b -> Decoder c -> Decoder d -> Decoder e -> Decoder f
+tuple5 f (Decoder fa) (Decoder fb) (Decoder fc) (Decoder fd) (Decoder fe) =
+  Decoder
+    ( Aeson.withArray "" \v ->
+        if Vector.length v == 5
+          then
+            f
+              <$> fa (Vector.unsafeIndex v 0)
+              <*> fb (Vector.unsafeIndex v 1)
+              <*> fc (Vector.unsafeIndex v 2)
+              <*> fd (Vector.unsafeIndex v 3)
+              <*> fe (Vector.unsafeIndex v 4)
+          else
+            fail ("expected 5-element array, but found " ++ show (Vector.length v))
+    )
+{-# INLINEABLE tuple5 #-}
+
+-- | A 6-tuple decoder.
+tuple6 ::
+  (a -> b -> c -> d -> e -> f -> g) ->
+  Decoder a ->
+  Decoder b ->
+  Decoder c ->
+  Decoder d ->
+  Decoder e ->
+  Decoder f ->
+  Decoder g
+tuple6 f (Decoder fa) (Decoder fb) (Decoder fc) (Decoder fd) (Decoder fe) (Decoder ff) =
+  Decoder
+    ( Aeson.withArray "" \v ->
+        if Vector.length v == 6
+          then
+            f
+              <$> fa (Vector.unsafeIndex v 0)
+              <*> fb (Vector.unsafeIndex v 1)
+              <*> fc (Vector.unsafeIndex v 2)
+              <*> fd (Vector.unsafeIndex v 3)
+              <*> fe (Vector.unsafeIndex v 4)
+              <*> ff (Vector.unsafeIndex v 5)
+          else
+            fail ("expected 6-element array, but found " ++ show (Vector.length v))
+    )
+{-# INLINEABLE tuple6 #-}
+
+-- | A 7-tuple decoder.
+tuple7 ::
+  (a -> b -> c -> d -> e -> f -> g -> h) ->
+  Decoder a ->
+  Decoder b ->
+  Decoder c ->
+  Decoder d ->
+  Decoder e ->
+  Decoder f ->
+  Decoder g ->
+  Decoder h
+tuple7 f (Decoder fa) (Decoder fb) (Decoder fc) (Decoder fd) (Decoder fe) (Decoder ff) (Decoder fg) =
+  Decoder
+    ( Aeson.withArray "" \v ->
+        if Vector.length v == 7
+          then
+            f
+              <$> fa (Vector.unsafeIndex v 0)
+              <*> fb (Vector.unsafeIndex v 1)
+              <*> fc (Vector.unsafeIndex v 2)
+              <*> fd (Vector.unsafeIndex v 3)
+              <*> fe (Vector.unsafeIndex v 4)
+              <*> ff (Vector.unsafeIndex v 5)
+              <*> fg (Vector.unsafeIndex v 6)
+          else
+            fail ("expected 7-element array, but found " ++ show (Vector.length v))
+    )
+{-# INLINEABLE tuple7 #-}
+
+-- | A 8-tuple decoder.
+tuple8 ::
+  (a -> b -> c -> d -> e -> f -> g -> h -> i) ->
+  Decoder a ->
+  Decoder b ->
+  Decoder c ->
+  Decoder d ->
+  Decoder e ->
+  Decoder f ->
+  Decoder g ->
+  Decoder h ->
+  Decoder i
+tuple8 f (Decoder fa) (Decoder fb) (Decoder fc) (Decoder fd) (Decoder fe) (Decoder ff) (Decoder fg) (Decoder fh) =
+  Decoder
+    ( Aeson.withArray "" \v ->
+        if Vector.length v == 8
+          then
+            f
+              <$> fa (Vector.unsafeIndex v 0)
+              <*> fb (Vector.unsafeIndex v 1)
+              <*> fc (Vector.unsafeIndex v 2)
+              <*> fd (Vector.unsafeIndex v 3)
+              <*> fe (Vector.unsafeIndex v 4)
+              <*> ff (Vector.unsafeIndex v 5)
+              <*> fg (Vector.unsafeIndex v 6)
+              <*> fh (Vector.unsafeIndex v 7)
+          else
+            fail ("expected 8-element array, but found " ++ show (Vector.length v))
+    )
+{-# INLINEABLE tuple8 #-}
+
+-- | A 9-tuple decoder.
+tuple9 ::
+  (a -> b -> c -> d -> e -> f -> g -> h -> i -> j) ->
+  Decoder a ->
+  Decoder b ->
+  Decoder c ->
+  Decoder d ->
+  Decoder e ->
+  Decoder f ->
+  Decoder g ->
+  Decoder h ->
+  Decoder i ->
+  Decoder j
+tuple9 f (Decoder fa) (Decoder fb) (Decoder fc) (Decoder fd) (Decoder fe) (Decoder ff) (Decoder fg) (Decoder fh) (Decoder fi) =
+  Decoder
+    ( Aeson.withArray "" \v ->
+        if Vector.length v == 9
+          then
+            f
+              <$> fa (Vector.unsafeIndex v 0)
+              <*> fb (Vector.unsafeIndex v 1)
+              <*> fc (Vector.unsafeIndex v 2)
+              <*> fd (Vector.unsafeIndex v 3)
+              <*> fe (Vector.unsafeIndex v 4)
+              <*> ff (Vector.unsafeIndex v 5)
+              <*> fg (Vector.unsafeIndex v 6)
+              <*> fh (Vector.unsafeIndex v 7)
+              <*> fi (Vector.unsafeIndex v 8)
+          else
+            fail ("expected 9-element array, but found " ++ show (Vector.length v))
+    )
+{-# INLINEABLE tuple9 #-}
 
 -- | An object decoder.
 object :: ObjectDecoder a -> Decoder a
diff --git a/src/Cretheus/Internal/Encode.hs b/src/Cretheus/Internal/Encode.hs
--- a/src/Cretheus/Internal/Encode.hs
+++ b/src/Cretheus/Internal/Encode.hs
@@ -22,14 +22,24 @@
     property,
     set,
     text,
+    tuple2,
+    tuple3,
+    tuple4,
+    tuple5,
+    tuple6,
+    tuple7,
+    tuple8,
+    tuple9,
     utcTime,
     value,
     vector,
   )
 where
 
+import Control.Monad.ST (runST)
 import Data.Aeson qualified as Aeson
 import Data.Aeson.Encoding qualified as Aeson
+import Data.Aeson.Encoding.Internal qualified as Aeson (Encoding' (..), closeBracket, comma, openBracket)
 import Data.Aeson.Key qualified as Aeson.Key
 import Data.Aeson.KeyMap qualified as Aeson (KeyMap)
 import Data.Aeson.KeyMap qualified as Aeson.KeyMap
@@ -50,6 +60,7 @@
 import Data.Time (UTCTime)
 import Data.Vector (Vector)
 import Data.Vector qualified as Vector
+import Data.Vector.Mutable qualified as Vector.Mutable
 import Prelude hiding (map, null)
 
 -- | A value encoding.
@@ -178,6 +189,271 @@
   list f . Set.toList
 {-# INLINEABLE set #-}
 
+-- | A 2-tuple encoder.
+tuple2 :: (a -> Encoding) -> (b -> Encoding) -> (a, b) -> Encoding
+tuple2 fa fb =
+  mk
+    ( \(a, b) ->
+        Aeson.openBracket
+          >< asAesonEncoding (fa a)
+          >< Aeson.comma
+          >< asAesonEncoding (fb b)
+          >< Aeson.closeBracket
+    )
+    ( \(a, b) ->
+        Aeson.Array
+          ( runST do
+              v <- Vector.Mutable.unsafeNew 2
+              Vector.Mutable.write v 0 (asValue (fa a))
+              Vector.Mutable.write v 1 (asValue (fb b))
+              Vector.unsafeFreeze v
+          )
+    )
+{-# INLINEABLE tuple2 #-}
+
+-- | A 3-tuple encoder.
+tuple3 :: (a -> Encoding) -> (b -> Encoding) -> (c -> Encoding) -> (a, b, c) -> Encoding
+tuple3 fa fb fc =
+  mk
+    ( \(a, b, c) ->
+        Aeson.openBracket
+          >< asAesonEncoding (fa a)
+          >< foldr (\x acc -> Aeson.comma >< asAesonEncoding x >< acc) Aeson.closeBracket [fb b, fc c]
+    )
+    ( \(a, b, c) ->
+        Aeson.Array
+          ( runST do
+              v <- Vector.Mutable.unsafeNew 3
+              Vector.Mutable.write v 0 (asValue (fa a))
+              Vector.Mutable.write v 1 (asValue (fb b))
+              Vector.Mutable.write v 2 (asValue (fc c))
+              Vector.unsafeFreeze v
+          )
+    )
+{-# INLINEABLE tuple3 #-}
+
+-- | A 4-tuple encoder.
+tuple4 :: (a -> Encoding) -> (b -> Encoding) -> (c -> Encoding) -> (d -> Encoding) -> (a, b, c, d) -> Encoding
+tuple4 fa fb fc fd =
+  mk
+    ( \(a, b, c, d) ->
+        Aeson.openBracket
+          >< asAesonEncoding (fa a)
+          >< foldr (\x acc -> Aeson.comma >< asAesonEncoding x >< acc) Aeson.closeBracket [fb b, fc c, fd d]
+    )
+    ( \(a, b, c, d) ->
+        Aeson.Array
+          ( runST do
+              v <- Vector.Mutable.unsafeNew 4
+              Vector.Mutable.write v 0 (asValue (fa a))
+              Vector.Mutable.write v 1 (asValue (fb b))
+              Vector.Mutable.write v 2 (asValue (fc c))
+              Vector.Mutable.write v 3 (asValue (fd d))
+              Vector.unsafeFreeze v
+          )
+    )
+{-# INLINEABLE tuple4 #-}
+
+-- | A 5-tuple encoder.
+tuple5 ::
+  (a -> Encoding) ->
+  (b -> Encoding) ->
+  (c -> Encoding) ->
+  (d -> Encoding) ->
+  (e -> Encoding) ->
+  (a, b, c, d, e) ->
+  Encoding
+tuple5 fa fb fc fd fe =
+  mk
+    ( \(a, b, c, d, e) ->
+        Aeson.openBracket
+          >< asAesonEncoding (fa a)
+          >< foldr (\x acc -> Aeson.comma >< asAesonEncoding x >< acc) Aeson.closeBracket [fb b, fc c, fd d, fe e]
+    )
+    ( \(a, b, c, d, e) ->
+        Aeson.Array
+          ( runST do
+              v <- Vector.Mutable.unsafeNew 5
+              Vector.Mutable.write v 0 (asValue (fa a))
+              Vector.Mutable.write v 1 (asValue (fb b))
+              Vector.Mutable.write v 2 (asValue (fc c))
+              Vector.Mutable.write v 3 (asValue (fd d))
+              Vector.Mutable.write v 4 (asValue (fe e))
+              Vector.unsafeFreeze v
+          )
+    )
+{-# INLINEABLE tuple5 #-}
+
+-- | A 6-tuple encoder.
+tuple6 ::
+  (a -> Encoding) ->
+  (b -> Encoding) ->
+  (c -> Encoding) ->
+  (d -> Encoding) ->
+  (e -> Encoding) ->
+  (f -> Encoding) ->
+  (a, b, c, d, e, f) ->
+  Encoding
+tuple6 fa fb fc fd fe ff =
+  mk
+    ( \(a, b, c, d, e, f) ->
+        Aeson.openBracket
+          >< asAesonEncoding (fa a)
+          >< foldr (\x acc -> Aeson.comma >< asAesonEncoding x >< acc) Aeson.closeBracket [fb b, fc c, fd d, fe e, ff f]
+    )
+    ( \(a, b, c, d, e, f) ->
+        Aeson.Array
+          ( runST do
+              v <- Vector.Mutable.unsafeNew 6
+              Vector.Mutable.write v 0 (asValue (fa a))
+              Vector.Mutable.write v 1 (asValue (fb b))
+              Vector.Mutable.write v 2 (asValue (fc c))
+              Vector.Mutable.write v 3 (asValue (fd d))
+              Vector.Mutable.write v 4 (asValue (fe e))
+              Vector.Mutable.write v 5 (asValue (ff f))
+              Vector.unsafeFreeze v
+          )
+    )
+{-# INLINEABLE tuple6 #-}
+
+-- | A 7-tuple encoder.
+tuple7 ::
+  (a -> Encoding) ->
+  (b -> Encoding) ->
+  (c -> Encoding) ->
+  (d -> Encoding) ->
+  (e -> Encoding) ->
+  (f -> Encoding) ->
+  (g -> Encoding) ->
+  (a, b, c, d, e, f, g) ->
+  Encoding
+tuple7 fa fb fc fd fe ff fg =
+  mk
+    ( \(a, b, c, d, e, f, g) ->
+        Aeson.openBracket
+          >< asAesonEncoding (fa a)
+          >< foldr
+            (\x acc -> Aeson.comma >< asAesonEncoding x >< acc)
+            Aeson.closeBracket
+            [ fb b,
+              fc c,
+              fd d,
+              fe e,
+              ff f,
+              fg g
+            ]
+    )
+    ( \(a, b, c, d, e, f, g) ->
+        Aeson.Array
+          ( runST do
+              v <- Vector.Mutable.unsafeNew 7
+              Vector.Mutable.write v 0 (asValue (fa a))
+              Vector.Mutable.write v 1 (asValue (fb b))
+              Vector.Mutable.write v 2 (asValue (fc c))
+              Vector.Mutable.write v 3 (asValue (fd d))
+              Vector.Mutable.write v 4 (asValue (fe e))
+              Vector.Mutable.write v 5 (asValue (ff f))
+              Vector.Mutable.write v 6 (asValue (fg g))
+              Vector.unsafeFreeze v
+          )
+    )
+{-# INLINEABLE tuple7 #-}
+
+-- | A 8-tuple encoder.
+tuple8 ::
+  (a -> Encoding) ->
+  (b -> Encoding) ->
+  (c -> Encoding) ->
+  (d -> Encoding) ->
+  (e -> Encoding) ->
+  (f -> Encoding) ->
+  (g -> Encoding) ->
+  (h -> Encoding) ->
+  (a, b, c, d, e, f, g, h) ->
+  Encoding
+tuple8 fa fb fc fd fe ff fg fh =
+  mk
+    ( \(a, b, c, d, e, f, g, h) ->
+        Aeson.openBracket
+          >< asAesonEncoding (fa a)
+          >< foldr
+            (\x acc -> Aeson.comma >< asAesonEncoding x >< acc)
+            Aeson.closeBracket
+            [ fb b,
+              fc c,
+              fd d,
+              fe e,
+              ff f,
+              fg g,
+              fh h
+            ]
+    )
+    ( \(a, b, c, d, e, f, g, h) ->
+        Aeson.Array
+          ( runST do
+              v <- Vector.Mutable.unsafeNew 8
+              Vector.Mutable.write v 0 (asValue (fa a))
+              Vector.Mutable.write v 1 (asValue (fb b))
+              Vector.Mutable.write v 2 (asValue (fc c))
+              Vector.Mutable.write v 3 (asValue (fd d))
+              Vector.Mutable.write v 4 (asValue (fe e))
+              Vector.Mutable.write v 5 (asValue (ff f))
+              Vector.Mutable.write v 6 (asValue (fg g))
+              Vector.Mutable.write v 7 (asValue (fh h))
+              Vector.unsafeFreeze v
+          )
+    )
+{-# INLINEABLE tuple8 #-}
+
+-- | A 9-tuple encoder.
+tuple9 ::
+  (a -> Encoding) ->
+  (b -> Encoding) ->
+  (c -> Encoding) ->
+  (d -> Encoding) ->
+  (e -> Encoding) ->
+  (f -> Encoding) ->
+  (g -> Encoding) ->
+  (h -> Encoding) ->
+  (i -> Encoding) ->
+  (a, b, c, d, e, f, g, h, i) ->
+  Encoding
+tuple9 fa fb fc fd fe ff fg fh fi =
+  mk
+    ( \(a, b, c, d, e, f, g, h, i) ->
+        Aeson.openBracket
+          >< asAesonEncoding (fa a)
+          >< foldr
+            (\x acc -> Aeson.comma >< asAesonEncoding x >< acc)
+            Aeson.closeBracket
+            [ fb b,
+              fc c,
+              fd d,
+              fe e,
+              ff f,
+              fg g,
+              fh h,
+              fi i
+            ]
+    )
+    ( \(a, b, c, d, e, f, g, h, i) ->
+        Aeson.Array
+          ( runST do
+              v <- Vector.Mutable.unsafeNew 9
+              Vector.Mutable.write v 0 (asValue (fa a))
+              Vector.Mutable.write v 1 (asValue (fb b))
+              Vector.Mutable.write v 2 (asValue (fc c))
+              Vector.Mutable.write v 3 (asValue (fd d))
+              Vector.Mutable.write v 4 (asValue (fe e))
+              Vector.Mutable.write v 5 (asValue (ff f))
+              Vector.Mutable.write v 6 (asValue (fg g))
+              Vector.Mutable.write v 7 (asValue (fh h))
+              Vector.Mutable.write v 8 (asValue (fi i))
+              Vector.unsafeFreeze v
+          )
+    )
+{-# INLINEABLE tuple9 #-}
+
 -- | An object property encoding.
 data PropertyEncoding
   = Algo !Prop
@@ -241,3 +517,10 @@
     (Aeson.dict (Aeson.text . Aeson.Key.toText) (asAesonEncoding . f) Aeson.KeyMap.foldrWithKey)
     (Aeson.Object . Aeson.KeyMap.map (asValue . f))
 {-# INLINEABLE keyMap #-}
+
+-- Copied from aeson source because they don't provide a Semigroup on Encoding...
+
+infixr 6 ><
+
+(><) :: Aeson.Encoding' a -> Aeson.Encoding' a -> Aeson.Encoding' a
+Aeson.Encoding a >< Aeson.Encoding b = Aeson.Encoding (a <> b)
