diff --git a/core-data.cabal b/core-data.cabal
--- a/core-data.cabal
+++ b/core-data.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           core-data
-version:        0.3.2.1
+version:        0.3.2.2
 synopsis:       Convenience wrappers around common data structures and encodings
 description:    Wrappers around common data structures and encodings.
                 .
@@ -46,7 +46,7 @@
     , base >=4.11 && <5
     , bytestring
     , containers
-    , core-text >=0.3.4
+    , core-text >=0.3.7
     , hashable >=1.2
     , prettyprinter >=1.6.2
     , scientific
diff --git a/lib/Core/Encoding/Json.hs b/lib/Core/Encoding/Json.hs
--- a/lib/Core/Encoding/Json.hs
+++ b/lib/Core/Encoding/Json.hs
@@ -67,6 +67,13 @@
     prettyValue,
 ) where
 
+#if MIN_VERSION_aeson(2,0,1)
+import qualified Data.Aeson.Key as Aeson
+import qualified Data.Aeson.KeyMap as Aeson
+#else
+import qualified Data.HashMap.Strict as HashMap
+#endif
+
 import Core.Data.Structures (Key, Map, fromMap, intoMap)
 import Core.Text.Bytes (Bytes, fromBytes, intoBytes)
 import Core.Text.Colour (
@@ -86,21 +93,15 @@
     fromRope,
     intoRope,
     singletonRope,
+    unconsRope,
  )
 import Core.Text.Utilities (
     Render (Token, colourize, highlight),
-    breakPieces,
+    breakRope,
  )
-import qualified Data.Aeson as Aeson
-
-#if MIN_VERSION_aeson(2,0,1)
-import qualified Data.Aeson.Key as Aeson
-import qualified Data.Aeson.KeyMap as Aeson
-#else
-import qualified Data.HashMap.Strict as HashMap
-#endif
-
 import Data.Aeson (FromJSON, Value (String))
+import qualified Data.Aeson as Aeson
+import Data.Char (intToDigit)
 import Data.Coerce
 import Data.Hashable (Hashable)
 import qualified Data.List as List
@@ -178,26 +179,34 @@
     closebracket = singletonRope ']'
 
 {- |
-Escape any quotes or backslashes in a JsonString.
+Escape any quotes, backslashes, or other possible rubbish in a 'JsonString'.
 -}
 escapeString :: Rope -> Rope
 escapeString text =
-    let text1 = escapeBackslashes text
-        text2 = escapeQuotes text1
-      in text2
+    let (before, after) = breakRope needsEscaping text
+     in case unconsRope after of
+            Nothing ->
+                text
+            Just (c, after') ->
+                before <> escapeCharacter c <> escapeString after'
+  where
+    needsEscaping c =
+        c == '\"' || c == '\\' || c < '\x20'
 {-# INLINEABLE escapeString #-}
 
-escapeBackslashes :: Rope -> Rope
-escapeBackslashes text =
-     let pieces = breakPieces (== '\\') text
-     in mconcat (List.intersperse "\\\\" pieces)
-{-# INLINEABLE escapeBackslashes #-}
-
-escapeQuotes :: Rope -> Rope
-escapeQuotes text =
-    let pieces = breakPieces (== '"') text
-     in mconcat (List.intersperse "\\\"" pieces)
-{-# INLINEABLE escapeQuotes #-}
+escapeCharacter :: Char -> Rope
+escapeCharacter c =
+    case c of
+        '\"' -> "\\\""
+        '\\' -> "\\\\"
+        '\n' -> "\\n"
+        '\r' -> "\\r"
+        '\t' -> "\\t"
+        _ ->
+            if c < '\x10'
+                then "\\u000" <> singletonRope (intToDigit (fromEnum c))
+                else "\\u001" <> singletonRope (intToDigit ((fromEnum c) - 16))
+{-# INLINEABLE escapeCharacter #-}
 
 {- |
 Given an array of bytes, attempt to decode it as a JSON value.
