diff --git a/dataframe-expr-serializer.cabal b/dataframe-expr-serializer.cabal
--- a/dataframe-expr-serializer.cabal
+++ b/dataframe-expr-serializer.cabal
@@ -1,6 +1,6 @@
 cabal-version:      3.4
 name:               dataframe-expr-serializer
-version:            1.2.1.0
+version:            1.3.0.0
 synopsis:           Serialize and deserialize dataframe expressions and pipelines.
 description:
     Save fitted @Expr@ values and whole pipelines (ordered @[NamedExpr]@) to a
@@ -39,8 +39,8 @@
     build-depends:      base >= 4 && < 5,
                         aeson >= 0.11.0.0 && < 3,
                         bytestring >= 0.11 && < 0.14,
-                        dataframe-core >= 2.5 && < 2.6,
-                        dataframe-operations >= 2.5 && < 2.6,
+                        dataframe-core >= 2.6 && < 2.7,
+                        dataframe-operations >= 2.6 && < 2.7,
                         text >= 2.1 && < 3,
                         vector >= 0.13 && < 0.15
     hs-source-dirs:     src
diff --git a/src/DataFrame/Expr/Serialize.hs b/src/DataFrame/Expr/Serialize.hs
--- a/src/DataFrame/Expr/Serialize.hs
+++ b/src/DataFrame/Expr/Serialize.hs
@@ -5,25 +5,7 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
 
-{- | Persist dataframe expressions and pipelines, and load them back for
-inference in another process (or from Python — see "DataFrame.IR.ExprJson" for
-the shared wire format).
-
-  * A single 'Expr' round-trips via 'encodeExpr' / 'decodeExprAny' (or the
-    type-asserting 'decodeExprAt') and the file helpers below.
-  * A /pipeline/ is an ordered @['NamedExpr']@ — the same shape a fitted
-    @Transform@ carries. 'encodeNamedExprs' / 'decodeNamedExprs' wrap it in a
-    versioned envelope; replay a decoded pipeline for inference with
-    @deriveMany@ / @applyTransform@.
-
-All functions are pure 'Either'; the @save@ helpers report an encode failure
-before touching the file (no partial write), and the @load@ helpers turn a
-missing/unreadable file into a 'Left' rather than throwing.
-
-Limitations inherited from the wire format: @CastWith@ / @CastExprWith@ (they
-hold closures) and @collect@ (its list-typed output has no type tag) cannot be
-serialized.
--}
+-- | Save and load dataframe expressions and pipelines for inference elsewhere.
 module DataFrame.Expr.Serialize (
     -- * Single expressions (re-exported wire codec)
     SomeExpr (..),
@@ -64,22 +46,20 @@
     parseSomeExpr,
  )
 import DataFrame.Internal.Column (Columnable)
-import DataFrame.Internal.Expression (Expr, NamedExpr, UExpr (..))
+import DataFrame.Internal.Expression (Expr, NamedExpr)
 
 -- | Decode a single expression from a strict JSON 'BS.ByteString'.
 decodeExprFromBytes :: BS.ByteString -> Either String SomeExpr
 decodeExprFromBytes bs = Aeson.eitherDecodeStrict bs >>= decodeExprAny
 
-{- | Encode an ordered pipeline (named output expressions) into the versioned
-envelope @{ "version": 1, "outputs": [ { "name", "expr" }, … ] }@.
--}
+-- | Encode an ordered pipeline of named expressions into a versioned envelope.
 encodeNamedExprs :: [NamedExpr] -> Either String Aeson.Value
 encodeNamedExprs nes = do
     outs <- traverse encodeOne nes
     Right $ object ["version" .= (1 :: Int), "outputs" .= outs]
   where
     encodeOne :: NamedExpr -> Either String Aeson.Value
-    encodeOne (name, UExpr e) = do
+    encodeOne (name, SomeExpr e) = do
         ev <- encodeExpr e
         Right $ object ["name" .= name, "expr" .= ev]
 
@@ -98,8 +78,8 @@
     parseOne = Aeson.withObject "PipelineOutput" $ \o -> do
         name <- o .: "name" :: Aeson.Parser T.Text
         rawExpr <- o .: "expr"
-        SomeExpr _ e <- parseSomeExpr rawExpr
-        return (name, UExpr e)
+        SomeExpr e <- parseSomeExpr rawExpr
+        return (name, SomeExpr e)
 
 -- | Encode an expression and write it to a file. No file is written on failure.
 saveExprToFile :: (Columnable a) => FilePath -> Expr a -> IO (Either String ())
@@ -111,9 +91,7 @@
 loadExprFromFile :: FilePath -> IO (Either String SomeExpr)
 loadExprFromFile fp = (>>= decodeExprFromBytes) <$> readBytes fp
 
-{- | Load an expression from a file, asserting its output type. Fails with
-'Left' if the stored expression has a different output type.
--}
+-- | Load an expression from a file, failing if its output type differs.
 loadExprAtFromFile ::
     forall a. (Columnable a) => FilePath -> IO (Either String (Expr a))
 loadExprAtFromFile fp = (>>= decode) <$> readBytes fp
diff --git a/src/DataFrame/IR/ExprJson.hs b/src/DataFrame/IR/ExprJson.hs
--- a/src/DataFrame/IR/ExprJson.hs
+++ b/src/DataFrame/IR/ExprJson.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE ConstraintKinds #-}
 {-# LANGUAGE ExplicitNamespaces #-}
 {-# LANGUAGE FlexibleContexts #-}
 {-# LANGUAGE GADTs #-}
@@ -7,29 +8,7 @@
 {-# LANGUAGE ScopedTypeVariables #-}
 {-# LANGUAGE TypeApplications #-}
 
-{- | JSON wire format for 'Expr' values.
-
-  The Python bindings build expression trees as JSON; this module decodes them
-  back into 'Expr' GADTs (typed) and encodes them in the reverse direction
-  (used for shipping fitted decision trees back to Python).
-
-  Encoded shape:
-
-  > { "node": "col"   , "out_type": "double", "name": "x" }
-  > { "node": "lit"   , "out_type": "int"   , "value": 42 }
-  > { "node": "unary" , "out_type": "double", "op": "toDouble"
-  >                   , "arg_type": "int"   , "arg": <expr> }
-  > { "node": "binary", "out_type": "bool"  , "op": "leq"
-  >                   , "arg_type": "double", "lhs": <expr>, "rhs": <expr> }
-  > { "node": "if"    , "out_type": "text"  , "cond": <expr>
-  >                   , "then": <expr>, "else": <expr> }
-
-  Operator names follow 'binaryName' / 'unaryName' from
-  "DataFrame.Internal.Expression". Nullable variants ("nulladd", "nullor", …)
-  are accepted on decode and treated as their non-null equivalents — Python
-  data crosses the FFI boundary as non-null Arrow buffers, so this is lossless
-  for the supported workflow.
--}
+-- | JSON wire format for 'Expr' values, decoding and encoding.
 module DataFrame.IR.ExprJson (
     SomeExpr (..),
     encodeExpr,
@@ -57,7 +36,7 @@
 import Data.Type.Equality (TestEquality (testEquality), type (:~:) (Refl))
 import qualified Data.Vector.Unboxed as VU
 import Data.Word (Word16, Word32, Word64, Word8)
-import Type.Reflection (TypeRep, Typeable, typeRep)
+import Type.Reflection (Typeable, typeRep)
 
 import DataFrame.Expression.Operators (
     ifThenElse,
@@ -76,15 +55,20 @@
     AggStrategy (..),
     BinaryOp (binaryName),
     Expr (..),
+    SomeExpr (..),
     UnaryOp (unaryName),
  )
 import qualified DataFrame.Internal.Expression as F
-
-{- | Existential wrapper around a typed expression decoded from JSON.
-TODO: mchavinda - Maybe consolidate with UExpr from the main package.
--}
-data SomeExpr where
-    SomeExpr :: (Columnable a) => TypeRep a -> Expr a -> SomeExpr
+import DataFrame.Internal.Expression.Reflect (
+    Dict (..),
+    RealUnbox,
+    floatingDict,
+    fracDict,
+    numDict,
+    ordDict,
+    realDict,
+    realUnboxDict,
+ )
 
 -- | Map a Haskell type to its wire-format tag string.
 typeTagOf :: forall a. (Typeable a) => Maybe T.Text
@@ -133,131 +117,63 @@
     "string" -> k (Proxy @String)
     _ -> fail $ "DataFrame.IR.ExprJson: unknown type tag: " <> T.unpack t
 
--- | Subset that supports 'Ord' (for comparison ops).
+-- | A tag whose type has the class evidence @dict@ provides.
+withClassTag ::
+    forall c r.
+    (forall a. (Columnable a) => Maybe (Dict (c a))) ->
+    String ->
+    T.Text ->
+    (forall a. (Columnable a, c a) => Proxy a -> Aeson.Parser r) ->
+    Aeson.Parser r
+withClassTag dict what t k = withTypeTag t $ \(p :: Proxy a) -> case dict @a of
+    Just Dict -> k p
+    Nothing ->
+        fail $
+            "DataFrame.IR.ExprJson: type does not support " <> what <> ": " <> T.unpack t
+
+-- | Types that support 'Ord' (comparison ops).
 withOrdTypeTag ::
     T.Text ->
     (forall a. (Columnable a, Ord a) => Proxy a -> Aeson.Parser r) ->
     Aeson.Parser r
-withOrdTypeTag t k = case t of
-    "int" -> k (Proxy @Int)
-    "int8" -> k (Proxy @Int8)
-    "int16" -> k (Proxy @Int16)
-    "int32" -> k (Proxy @Int32)
-    "int64" -> k (Proxy @Int64)
-    "word" -> k (Proxy @Word)
-    "word8" -> k (Proxy @Word8)
-    "word16" -> k (Proxy @Word16)
-    "word32" -> k (Proxy @Word32)
-    "word64" -> k (Proxy @Word64)
-    "integer" -> k (Proxy @Integer)
-    "double" -> k (Proxy @Double)
-    "float" -> k (Proxy @Float)
-    "bool" -> k (Proxy @Bool)
-    "char" -> k (Proxy @Char)
-    "text" -> k (Proxy @T.Text)
-    "string" -> k (Proxy @String)
-    _ ->
-        fail $ "DataFrame.IR.ExprJson: type does not support ordering: " <> T.unpack t
+withOrdTypeTag = withClassTag @Ord ordDict "ordering"
 
--- | Subset that supports 'Num' (arithmetic).
+-- | Types that support 'Num' (arithmetic).
 withNumTypeTag ::
     T.Text ->
     (forall a. (Columnable a, Num a) => Proxy a -> Aeson.Parser r) ->
     Aeson.Parser r
-withNumTypeTag t k = case t of
-    "int" -> k (Proxy @Int)
-    "int8" -> k (Proxy @Int8)
-    "int16" -> k (Proxy @Int16)
-    "int32" -> k (Proxy @Int32)
-    "int64" -> k (Proxy @Int64)
-    "word" -> k (Proxy @Word)
-    "word8" -> k (Proxy @Word8)
-    "word16" -> k (Proxy @Word16)
-    "word32" -> k (Proxy @Word32)
-    "word64" -> k (Proxy @Word64)
-    "integer" -> k (Proxy @Integer)
-    "double" -> k (Proxy @Double)
-    "float" -> k (Proxy @Float)
-    _ ->
-        fail $ "DataFrame.IR.ExprJson: type does not support arithmetic: " <> T.unpack t
+withNumTypeTag = withClassTag @Num numDict "arithmetic"
 
--- | Subset that supports 'Fractional' (division).
+-- | Types that support 'Fractional' (division).
 withFracTypeTag ::
     T.Text ->
     (forall a. (Columnable a, Fractional a) => Proxy a -> Aeson.Parser r) ->
     Aeson.Parser r
-withFracTypeTag t k = case t of
-    "double" -> k (Proxy @Double)
-    "float" -> k (Proxy @Float)
-    _ ->
-        fail $
-            "DataFrame.IR.ExprJson: type does not support fractional division: "
-                <> T.unpack t
+withFracTypeTag = withClassTag @Fractional fracDict "fractional division"
 
--- | Subset that supports 'Real' (used by toDouble source types).
+-- | Types that support 'Real' (toDouble sources).
 withRealTypeTag ::
     T.Text ->
     (forall a. (Columnable a, Real a) => Proxy a -> Aeson.Parser r) ->
     Aeson.Parser r
-withRealTypeTag t k = case t of
-    "int" -> k (Proxy @Int)
-    "int8" -> k (Proxy @Int8)
-    "int16" -> k (Proxy @Int16)
-    "int32" -> k (Proxy @Int32)
-    "int64" -> k (Proxy @Int64)
-    "word" -> k (Proxy @Word)
-    "word8" -> k (Proxy @Word8)
-    "word16" -> k (Proxy @Word16)
-    "word32" -> k (Proxy @Word32)
-    "word64" -> k (Proxy @Word64)
-    "integer" -> k (Proxy @Integer)
-    "double" -> k (Proxy @Double)
-    "float" -> k (Proxy @Float)
-    _ ->
-        fail $
-            "DataFrame.IR.ExprJson: type does not support Real (toDouble source): "
-                <> T.unpack t
+withRealTypeTag = withClassTag @Real realDict "Real (toDouble source)"
 
--- | Subset that supports 'Floating'.
+-- | Types that support 'Floating'.
 withFloatingTypeTag ::
     T.Text ->
     (forall a. (Columnable a, Floating a) => Proxy a -> Aeson.Parser r) ->
     Aeson.Parser r
-withFloatingTypeTag t k = case t of
-    "double" -> k (Proxy @Double)
-    "float" -> k (Proxy @Float)
-    _ ->
-        fail $ "DataFrame.IR.ExprJson: type does not support Floating: " <> T.unpack t
+withFloatingTypeTag = withClassTag @Floating floatingDict "Floating"
 
-{- | Subset that supports both 'Real' and 'VU.Unbox' (used by variance/median).
-'Integer' is deliberately omitted — it is 'Real' but not 'Unbox'.
--}
+-- | Types that support 'Real' and 'VU.Unbox' (variance, median).
 withRealUnboxTypeTag ::
     T.Text ->
     (forall a. (Columnable a, Real a, VU.Unbox a) => Proxy a -> Aeson.Parser r) ->
     Aeson.Parser r
-withRealUnboxTypeTag t k = case t of
-    "int" -> k (Proxy @Int)
-    "int8" -> k (Proxy @Int8)
-    "int16" -> k (Proxy @Int16)
-    "int32" -> k (Proxy @Int32)
-    "int64" -> k (Proxy @Int64)
-    "word" -> k (Proxy @Word)
-    "word8" -> k (Proxy @Word8)
-    "word16" -> k (Proxy @Word16)
-    "word32" -> k (Proxy @Word32)
-    "word64" -> k (Proxy @Word64)
-    "double" -> k (Proxy @Double)
-    "float" -> k (Proxy @Float)
-    _ ->
-        fail $
-            "DataFrame.IR.ExprJson: type does not support Real+Unbox (variance/median): "
-                <> T.unpack t
+withRealUnboxTypeTag = withClassTag @RealUnbox realUnboxDict "Real+Unbox (variance/median)"
 
-{- | Encode an 'Expr' to a JSON value. Returns 'Left' on unsupported
-constructors (Agg, Over, CastWith, CastExprWith) or unsupported operator
-names (binaryUdf, unaryUdf, …).
--}
+-- | Encode an 'Expr' to JSON, failing on unsupported constructors or operators.
 encodeExpr :: forall a. (Columnable a) => Expr a -> Either String Aeson.Value
 encodeExpr expr = case expr of
     Col name -> do
@@ -405,7 +321,7 @@
         Left $
             "DataFrame.IR.ExprJson.encodeLit: unsupported type: " <> show (typeRep @a)
 
--- | Map a 'binaryName' string to a wire-format op name. Errors on opaque UDF names.
+-- | Map a 'binaryName' string to a wire-format op name.
 recognizeBinary :: T.Text -> Either String T.Text
 recognizeBinary n = case n of
     "add" -> Right "add"
@@ -437,7 +353,7 @@
             "DataFrame.IR.ExprJson: unsupported binary op (cannot serialize): "
                 <> T.unpack other
 
--- | Map a 'unaryName' string to a wire-format op name. Errors on opaque UDF names.
+-- | Map a 'unaryName' string to a wire-format op name.
 recognizeUnary :: T.Text -> Either String T.Text
 recognizeUnary n = case n of
     "not" -> Right "not"
@@ -464,9 +380,7 @@
             "DataFrame.IR.ExprJson: unsupported unary op (cannot serialize): "
                 <> T.unpack other
 
-{- | Decode a JSON value into a 'SomeExpr'. The output type comes from the
-@out_type@ field on the root node.
--}
+-- | Decode a JSON value into a 'SomeExpr' using the root @out_type@.
 decodeExprAny :: Aeson.Value -> Either String SomeExpr
 decodeExprAny = Aeson.parseEither parseSomeExpr
 
@@ -474,15 +388,15 @@
 decodeExprAt ::
     forall a. (Columnable a) => Aeson.Value -> Either String (Expr a)
 decodeExprAt v = do
-    SomeExpr trep expr <- decodeExprAny v
-    case testEquality trep (typeRep @a) of
+    SomeExpr (expr :: Expr b) <- decodeExprAny v
+    case testEquality (typeRep @b) (typeRep @a) of
         Just Refl -> Right expr
         Nothing ->
             Left $
                 "DataFrame.IR.ExprJson.decodeExprAt: expected "
                     <> show (typeRep @a)
                     <> " but got "
-                    <> show trep
+                    <> show (typeRep @b)
 
 -- | The Aeson.Parser entry point — useful when composing with bigger parsers.
 parseSomeExpr :: Aeson.Value -> Aeson.Parser SomeExpr
@@ -493,12 +407,12 @@
         "col" -> do
             name <- o .: "name" :: Aeson.Parser T.Text
             withTypeTag outType $ \(_ :: Proxy a) ->
-                return $ SomeExpr (typeRep @a) (Col @a name)
+                return $ SomeExpr (Col @a name)
         "lit" -> do
             rawVal <- o .: "value"
             withTypeTag outType $ \(_ :: Proxy a) -> do
                 litVal <- decodeLit @a rawVal
-                return $ SomeExpr (typeRep @a) (Lit @a litVal)
+                return $ SomeExpr (Lit @a litVal)
         "if" -> do
             rawCond <- o .: "cond"
             rawThen <- o .: "then"
@@ -507,7 +421,7 @@
             withTypeTag outType $ \(_ :: Proxy a) -> do
                 thenE <- parseExprAt @a rawThen
                 elseE <- parseExprAt @a rawElse
-                return $ SomeExpr (typeRep @a) (ifThenElse cond thenE elseE)
+                return $ SomeExpr (ifThenElse cond thenE elseE)
         "unary" -> do
             op <- o .: "op" :: Aeson.Parser T.Text
             argType <- o .: "arg_type" :: Aeson.Parser T.Text
@@ -529,20 +443,20 @@
             rawArg <- o .: "arg"
             withTypeTag outType $ \(_ :: Proxy a) -> do
                 inner <- parseExprAt @a rawArg
-                return $ SomeExpr (typeRep @a) (F.over names inner)
+                return $ SomeExpr (F.over names inner)
         other -> fail $ "DataFrame.IR.ExprJson: unknown node kind: " <> T.unpack other
 
 parseExprAt :: forall a. (Columnable a) => Aeson.Value -> Aeson.Parser (Expr a)
 parseExprAt v = do
-    SomeExpr trep expr <- parseSomeExpr v
-    case testEquality trep (typeRep @a) of
+    SomeExpr (expr :: Expr b) <- parseSomeExpr v
+    case testEquality (typeRep @b) (typeRep @a) of
         Just Refl -> return expr
         Nothing ->
             fail $
                 "DataFrame.IR.ExprJson: expected "
                     <> show (typeRep @a)
                     <> " but got "
-                    <> show trep
+                    <> show (typeRep @b)
 
 decodeLit :: forall a. (Columnable a) => Aeson.Value -> Aeson.Parser a
 decodeLit v
@@ -606,24 +520,24 @@
         requireTag outType "bool"
         requireTag argType "bool"
         arg <- parseExprAt @Bool rawArg
-        return $ SomeExpr (typeRep @Bool) (F.not arg)
+        return $ SomeExpr (F.not arg)
     "negate" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         arg <- parseExprAt @a rawArg
-        return $ SomeExpr (typeRep @a) (negate arg)
+        return $ SomeExpr (negate arg)
     "abs" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         arg <- parseExprAt @a rawArg
-        return $ SomeExpr (typeRep @a) (abs arg)
+        return $ SomeExpr (abs arg)
     "signum" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         arg <- parseExprAt @a rawArg
-        return $ SomeExpr (typeRep @a) (signum arg)
+        return $ SomeExpr (signum arg)
     "toDouble" -> do
         requireTag outType "double"
         withRealTypeTag argType $ \(_ :: Proxy a) -> do
             arg <- parseExprAt @a rawArg
-            return $ SomeExpr (typeRep @Double) (F.toDouble arg)
+            return $ SomeExpr (F.toDouble arg)
     "exp" -> floatingUnary outType argType rawArg exp
     "sqrt" -> floatingUnary outType argType rawArg sqrt
     "log" -> floatingUnary outType argType rawArg log
@@ -649,7 +563,7 @@
 floatingUnary outType argType rawArg f = withFloatingTypeTag outType $ \(_ :: Proxy a) -> do
     requireSame outType argType
     arg <- parseExprAt @a rawArg
-    return $ SomeExpr (typeRep @a) (f arg)
+    return $ SomeExpr (f arg)
 
 parseBinary ::
     T.Text ->
@@ -664,72 +578,72 @@
         withTypeTag argType $ \(_ :: Proxy a) -> do
             l <- parseExprAt @a rawLhs
             r <- parseExprAt @a rawRhs
-            return $ SomeExpr (typeRep @Bool) (l .==. r)
+            return $ SomeExpr (l .==. r)
     "neq" -> do
         requireTag outType "bool"
         withTypeTag argType $ \(_ :: Proxy a) -> do
             l <- parseExprAt @a rawLhs
             r <- parseExprAt @a rawRhs
-            return $ SomeExpr (typeRep @Bool) (l ./=. r)
+            return $ SomeExpr (l ./=. r)
     "lt" -> do
         requireTag outType "bool"
         withOrdTypeTag argType $ \(_ :: Proxy a) -> do
             l <- parseExprAt @a rawLhs
             r <- parseExprAt @a rawRhs
-            return $ SomeExpr (typeRep @Bool) (l .<. r)
+            return $ SomeExpr (l .<. r)
     "leq" -> do
         requireTag outType "bool"
         withOrdTypeTag argType $ \(_ :: Proxy a) -> do
             l <- parseExprAt @a rawLhs
             r <- parseExprAt @a rawRhs
-            return $ SomeExpr (typeRep @Bool) (l .<=. r)
+            return $ SomeExpr (l .<=. r)
     "gt" -> do
         requireTag outType "bool"
         withOrdTypeTag argType $ \(_ :: Proxy a) -> do
             l <- parseExprAt @a rawLhs
             r <- parseExprAt @a rawRhs
-            return $ SomeExpr (typeRep @Bool) (l .>. r)
+            return $ SomeExpr (l .>. r)
     "geq" -> do
         requireTag outType "bool"
         withOrdTypeTag argType $ \(_ :: Proxy a) -> do
             l <- parseExprAt @a rawLhs
             r <- parseExprAt @a rawRhs
-            return $ SomeExpr (typeRep @Bool) (l .>=. r)
+            return $ SomeExpr (l .>=. r)
     "and" -> do
         requireTag outType "bool"
         l <- parseExprAt @Bool rawLhs
         r <- parseExprAt @Bool rawRhs
-        return $ SomeExpr (typeRep @Bool) (l .&&. r)
+        return $ SomeExpr (l .&&. r)
     "or" -> do
         requireTag outType "bool"
         l <- parseExprAt @Bool rawLhs
         r <- parseExprAt @Bool rawRhs
-        return $ SomeExpr (typeRep @Bool) (l .||. r)
+        return $ SomeExpr (l .||. r)
     "add" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         l <- parseExprAt @a rawLhs
         r <- parseExprAt @a rawRhs
-        return $ SomeExpr (typeRep @a) (l + r)
+        return $ SomeExpr (l + r)
     "sub" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         l <- parseExprAt @a rawLhs
         r <- parseExprAt @a rawRhs
-        return $ SomeExpr (typeRep @a) (l - r)
+        return $ SomeExpr (l - r)
     "mult" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         l <- parseExprAt @a rawLhs
         r <- parseExprAt @a rawRhs
-        return $ SomeExpr (typeRep @a) (l * r)
+        return $ SomeExpr (l * r)
     "divide" -> withFracTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         l <- parseExprAt @a rawLhs
         r <- parseExprAt @a rawRhs
-        return $ SomeExpr (typeRep @a) (l / r)
+        return $ SomeExpr (l / r)
     "exponentiate" -> withFloatingTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         l <- parseExprAt @a rawLhs
         r <- parseExprAt @a rawRhs
-        return $ SomeExpr (typeRep @a) (l ** r)
+        return $ SomeExpr (l ** r)
     "nulladd" -> parseBinary "add" outType argType rawLhs rawRhs
     "nullsub" -> parseBinary "sub" outType argType rawLhs rawRhs
     "nullmul" -> parseBinary "mult" outType argType rawLhs rawRhs
@@ -738,21 +652,18 @@
     "nullor" -> parseBinary "or" outType argType rawLhs rawRhs
     other -> fail $ "DataFrame.IR.ExprJson: unsupported binary op: " <> T.unpack other
 
-{- | Decode an aggregation node. Dispatches on the stored aggregation name to the
-matching 'DataFrame.Functions' builder, picking the type-tag dispatcher demanded
-by that builder's constraints.
--}
+-- | Decode an aggregation node by dispatching on its stored aggregation name.
 parseAgg :: T.Text -> T.Text -> T.Text -> Aeson.Value -> Aeson.Parser SomeExpr
 parseAgg aggName outType argType rawArg = case aggName of
     "count" -> do
         requireTag outType "int"
         withTypeTag argType $ \(_ :: Proxy a) -> do
             arg <- parseExprAt @a rawArg
-            return $ SomeExpr (typeRep @Int) (F.count arg)
+            return $ SomeExpr (F.count arg)
     "sum" -> withNumTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         arg <- parseExprAt @a rawArg
-        return $ SomeExpr (typeRep @a) (F.sum arg)
+        return $ SomeExpr (F.sum arg)
     "minimum" -> ordAgg F.minimum
     "maximum" -> ordAgg F.maximum
     "mode" -> ordAgg F.mode
@@ -760,7 +671,7 @@
         requireTag outType "double"
         withRealTypeTag argType $ \(_ :: Proxy a) -> do
             arg <- parseExprAt @a rawArg
-            return $ SomeExpr (typeRep @Double) (F.mean arg)
+            return $ SomeExpr (F.mean arg)
     "variance" -> realUnboxAgg F.variance
     "median" -> realUnboxAgg F.median
     "collect" ->
@@ -775,7 +686,7 @@
     ordAgg op = withOrdTypeTag outType $ \(_ :: Proxy a) -> do
         requireSame outType argType
         arg <- parseExprAt @a rawArg
-        return $ SomeExpr (typeRep @a) (op arg)
+        return $ SomeExpr (op arg)
     realUnboxAgg ::
         (forall a. (Columnable a, Real a, VU.Unbox a) => Expr a -> Expr Double) ->
         Aeson.Parser SomeExpr
@@ -783,4 +694,4 @@
         requireTag outType "double"
         withRealUnboxTypeTag argType $ \(_ :: Proxy a) -> do
             arg <- parseExprAt @a rawArg
-            return $ SomeExpr (typeRep @Double) (op arg)
+            return $ SomeExpr (op arg)
