diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,9 @@
 # ChangeLog for yesod-core
 
+## 1.6.14
+
+* Introduce `JSONResponse`. [issue #1481](https://github.com/yesodweb/yesod/issues/1481) and [PR #1592](https://github.com/yesodweb/yesod/pull/1592)
+
 ## 1.6.13
 
 * Introduce `maxContentLengthIO`. [issue #1588](https://github.com/yesodweb/yesod/issues/1588) and [PR #1589](https://github.com/yesodweb/yesod/pull/1589)
diff --git a/src/Yesod/Core/Content.hs b/src/Yesod/Core/Content.hs
--- a/src/Yesod/Core/Content.hs
+++ b/src/Yesod/Core/Content.hs
@@ -107,6 +107,8 @@
     toContent = snd
 instance ToContent TypedContent where
     toContent (TypedContent _ c) = c
+instance ToContent (JSONResponse a) where
+    toContent (JSONResponse a) = toContent $ J.toEncoding a
 
 instance ToContent Css where
     toContent = toContent . renderCss
@@ -160,6 +162,8 @@
 instance HasContentType RepPlain where
     getContentType _ = typePlain
 deriving instance ToContent RepPlain
+instance HasContentType (JSONResponse a) where
+    getContentType _ = typeJson
 
 instance HasContentType RepXml where
     getContentType _ = typeXml
@@ -292,6 +296,8 @@
     toTypedContent = toTypedContent . pack
 instance ToTypedContent Text where
     toTypedContent t = TypedContent typePlain (toContent t)
+instance ToTypedContent (JSONResponse a) where
+    toTypedContent c = TypedContent typeJson (toContent c)
 instance ToTypedContent a => ToTypedContent (DontFullyEvaluate a) where
     toTypedContent (DontFullyEvaluate a) =
         let TypedContent ct c = toTypedContent a
diff --git a/src/Yesod/Core/Types.hs b/src/Yesod/Core/Types.hs
--- a/src/Yesod/Core/Types.hs
+++ b/src/Yesod/Core/Types.hs
@@ -8,8 +8,10 @@
 {-# LANGUAGE MultiParamTypeClasses      #-}
 {-# LANGUAGE TypeFamilies               #-}
 {-# LANGUAGE UndecidableInstances #-}
+{-# LANGUAGE GADTs #-}
 module Yesod.Core.Types where
 
+import Data.Aeson (ToJSON)
 import qualified Data.ByteString.Builder            as BB
 import           Control.Arrow                      (first)
 import           Control.Exception                  (Exception)
@@ -302,6 +304,20 @@
 newtype RepXml = RepXml Content
 
 type ContentType = ByteString -- FIXME Text?
+
+-- | Wrapper around types so that Handlers can return a domain type, even when
+-- the data will eventually be encoded as JSON.
+-- Example usage in a type signature:
+--
+-- > postSignupR :: Handler (JSONResponse CreateUserResponse)
+--
+-- And in the implementation:
+--
+-- > return $ JSONResponse $ CreateUserResponse userId
+--
+-- @since 1.6.14
+data JSONResponse a where
+    JSONResponse :: ToJSON a => a -> JSONResponse a
 
 -- | Prevents a response body from being fully evaluated before sending the
 -- request.
diff --git a/yesod-core.cabal b/yesod-core.cabal
--- a/yesod-core.cabal
+++ b/yesod-core.cabal
@@ -1,5 +1,5 @@
 name:            yesod-core
-version:         1.6.13
+version:         1.6.14
 license:         MIT
 license-file:    LICENSE
 author:          Michael Snoyman <michael@snoyman.com>
