diff --git a/aeson.cabal b/aeson.cabal
--- a/aeson.cabal
+++ b/aeson.cabal
@@ -1,6 +1,6 @@
 cabal-version:      2.2
 name:               aeson
-version:            2.2.5.0
+version:            2.3.0.0
 license:            BSD-3-Clause
 license-file:       LICENSE
 category:           Text, Web, JSON
@@ -130,7 +130,7 @@
     , semialign             ^>=1.3      || ^>=1.4
     , strict                ^>=0.5
     , tagged                ^>=0.8.7
-    , text-iso8601          ^>=0.1.1
+    , text-iso8601          >=0.1.1 && < 0.3
     , text-short            ^>=0.1.5
     , th-abstraction        ^>=0.5.0.0  || ^>=0.6.0.0 || ^>=0.7.0.0
     , these                 ^>=1.2
@@ -238,7 +238,7 @@
     build-depends: integer-gmp
 
   if impl(ghc >=9.2 && <9.7)
-    build-depends: nothunks >=0.1.4 && <0.3
+    build-depends: nothunks >=0.1.4 && <0.4
 
 source-repository head
   type:     git
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,16 @@
 For the latest version of this document, please see [https://github.com/haskell/aeson/blob/master/changelog.md](https://github.com/haskell/aeson/blob/master/changelog.md).
 
+### 2.3.0.0 - 2026-05-21
+
+* Fix parsing of fractional numbers to reject exponents smaller than -1024.
+  This breaking change affects `FromJSON` instances of `Fixed`, `DiffTime`, and `NominalDiffTime`,
+  rejecting more inputs. Error messages for `Ratio` and integral types are also slightly different
+  due to reusing the same bounding logic.
+* Fix typo in error message: "~~Unespected~~ Unexpected control character while parsing string literal".
+* Accept 24:00:00 time of day.
+* Support nothunks 0.3.
+* Unset executable permissions in some test files and remove a broken symlink.
+
 ### 2.2.5.0
 
 * Support `semialign-1.4`
@@ -34,7 +45,7 @@
   One gotcha is that internal `Text` values (in `Key`s or `Value` `String`s)
   will most likely retain the original input `Text` value (its underlying `Array`).
   It shouldn't be an issue if the `Value` is then decoded to something else so these
-  `Text` values disapper, but if not (e.g. `Object` keys survive)
+  `Text` values disappear, but if not (e.g. `Object` keys survive)
   then users might want to use `Data.Text.copy`.
 
 ### 2.2.0.0
@@ -54,7 +65,7 @@
 
   In addition to `Maybe` (and `Option`) fields the `Data.Monoid.First` and `Data.Monoid.Last` are also omitted,
   as well as the most newtype wrappers, when their wrap omittable type (e.g. newtypes in `Data.Monoid` and `Data.Semigroup`, `Identity`, `Const`, `Tagged`, `Compose`).
-  Additionall "boring" types like `()` and `Proxy` are omitted as well.
+  Additionally "boring" types like `()` and `Proxy` are omitted as well.
   As the omitting is now uniform, type arguments are also omitted (also in `Generic1` derived instance).
 
   Resolves issues:
@@ -72,7 +83,7 @@
 * Move `Data.Aeson.Parser` module into separate [`attoparsec-aeson`](https://hackage.haskell.org/package/attoparsec-aeson) package, as these parsers are not used by `aeson` itself anymore.
 * Use [`text-iso8601`](https://hackage.haskell.org/package/text-iso8601) package for parsing `time` types. These are slightly faster than previously used (copy of) `attoparsec-iso8601`.
   Formats accepted is slightly changed:
-  - The space between time and timezone offset (in `UTCTime` and `ZonedTime`) is disallowed. ISO8601 explictly forbidds it.
+  - The space between time and timezone offset (in `UTCTime` and `ZonedTime`) is disallowed. ISO8601 explicitly forbids it.
   - The timezone offsets can be in range -23:59..23:59. This is how Python, joda-time etc seems to do. (Previously the range was -12..+14)
 
 * Remove internal `Data.Aeson.Internal` and `Data.Aeson.Internal.Time` modules. Everything from the former is exported elsewhere (`Data.Aeson.Types`), the latter was truly internal.
@@ -235,7 +246,7 @@
   parseJSON = gParseJSON defaultOptions { rejectUnknownFields = True }
 ```
 
-* `FromJSON` instance of `Ratio a` now parses numbers in addtion to
+* `FromJSON` instance of `Ratio a` now parses numbers in addition to
   standard `{numerator=..., denumerator=...}` encoding. Thanks to
   Aleksey Khudyakov.
 
diff --git a/src/Data/Aeson/Decoding.hs b/src/Data/Aeson/Decoding.hs
--- a/src/Data/Aeson/Decoding.hs
+++ b/src/Data/Aeson/Decoding.hs
@@ -1,6 +1,6 @@
 {-# LANGUAGE RankNTypes          #-}
 {-# LANGUAGE ScopedTypeVariables #-}
--- | Convertion to and from @aeson@ 'A.Value'.
+-- | Conversion to and from @aeson@ 'A.Value'.
 -- 
 module Data.Aeson.Decoding (
     decode,
diff --git a/src/Data/Aeson/Decoding/ByteString.hs b/src/Data/Aeson/Decoding/ByteString.hs
--- a/src/Data/Aeson/Decoding/ByteString.hs
+++ b/src/Data/Aeson/Decoding/ByteString.hs
@@ -163,7 +163,7 @@
         Just (_, bs') -> goEsc (n + 1) bs'
 
     errEnd = err "Unexpected end-of-input while parsing string literal"
-    errCC  = err "Unespected control character while parsing string literal"
+    errCC  = err "Unexpected control character while parsing string literal"
 
 -------------------------------------------------------------------------------
 -- Number
diff --git a/src/Data/Aeson/Decoding/ByteString/Lazy.hs b/src/Data/Aeson/Decoding/ByteString/Lazy.hs
--- a/src/Data/Aeson/Decoding/ByteString/Lazy.hs
+++ b/src/Data/Aeson/Decoding/ByteString/Lazy.hs
@@ -169,7 +169,7 @@
         Just (_, bs') -> goEsc (n + 1) bs'
 
     errEnd = err "Unexpected end-of-input while parsing string literal"
-    errCC  = err "Unespected control character while parsing string literal"
+    errCC  = err "Unexpected control character while parsing string literal"
 
 -------------------------------------------------------------------------------
 -- Number
diff --git a/src/Data/Aeson/Decoding/Conversion.hs b/src/Data/Aeson/Decoding/Conversion.hs
--- a/src/Data/Aeson/Decoding/Conversion.hs
+++ b/src/Data/Aeson/Decoding/Conversion.hs
@@ -87,7 +87,7 @@
         -> (e -> r)
         -> ([(Key, A.Value)] -> k -> r)
         -> r
-    -- here we don't stricly need bang on !v as KM is a Strict (in values) map.
+    -- here we don't strictly need bang on !v as KM is a Strict (in values) map.
     -- but we force the value sooner.
     goR !acc (TkPair t toks) g f = convert toks g $ \ !v k -> goR ((t , v) : acc) k g f
     goR !acc (TkRecordEnd k) _ f = f acc k
diff --git a/src/Data/Aeson/Decoding/Text.hs b/src/Data/Aeson/Decoding/Text.hs
--- a/src/Data/Aeson/Decoding/Text.hs
+++ b/src/Data/Aeson/Decoding/Text.hs
@@ -173,7 +173,7 @@
         Just (_, bs') -> goEsc (n + 1) bs'
 
     errEnd = err "Unexpected end-of-input while parsing string literal"
-    errCC  = err "Unespected control character while parsing string literal"
+    errCC  = err "Unexpected control character while parsing string literal"
 
 -------------------------------------------------------------------------------
 -- Number
diff --git a/src/Data/Aeson/Internal/Unescape.hs b/src/Data/Aeson/Internal/Unescape.hs
--- a/src/Data/Aeson/Internal/Unescape.hs
+++ b/src/Data/Aeson/Internal/Unescape.hs
@@ -29,7 +29,7 @@
 
 -- | Unescape JSON text literal.
 --
--- This function is exporeted mostly for testing and benchmarking purposes.
+-- This function is exported mostly for testing and benchmarking purposes.
 unescapeText :: ByteString -> Either UnicodeException Text
 unescapeText = unsafeDupablePerformIO . try . unescapeTextIO
 
diff --git a/src/Data/Aeson/Internal/UnescapeFromText.hs b/src/Data/Aeson/Internal/UnescapeFromText.hs
--- a/src/Data/Aeson/Internal/UnescapeFromText.hs
+++ b/src/Data/Aeson/Internal/UnescapeFromText.hs
@@ -24,7 +24,7 @@
 
 -- | Unescape JSON text literal.
 --
--- This function is exporeted mostly for testing and benchmarking purposes.
+-- This function is exported mostly for testing and benchmarking purposes.
 unescapeFromText :: Text -> Either UnicodeException Text
 unescapeFromText = unsafeDupablePerformIO . try . unescapeFromTextIO
 
diff --git a/src/Data/Aeson/TH.hs b/src/Data/Aeson/TH.hs
--- a/src/Data/Aeson/TH.hs
+++ b/src/Data/Aeson/TH.hs
@@ -955,7 +955,7 @@
              | (field, argTy) <- zip fields argTys
              ]
 
--- A hack, as I'm too lazy to changge code to not assume fields are non empty.
+-- A hack, as I'm too lazy to change code to not assume fields are non empty.
 nonEmpty :: [a] -> (a, [a])
 nonEmpty (x:xs) = (x,xs)
 nonEmpty []     = error "unexpected empty list"
@@ -1276,7 +1276,7 @@
     varE $ case M.lookup tyName tvMap of
                 Just (tfjoExp, tfjExp, tfjlExp) -> case list of
                     Omit -> tfjoExp
-                    Single -> tfjExp 
+                    Single -> tfjExp
                     Plural -> tfjlExp
                 Nothing                   -> jsonFunValOrListName list jf Arity0
 dispatchFunByType jc jf conName tvMap list (SigT ty _) =
diff --git a/src/Data/Aeson/Types/FromJSON.hs b/src/Data/Aeson/Types/FromJSON.hs
--- a/src/Data/Aeson/Types/FromJSON.hs
+++ b/src/Data/Aeson/Types/FromJSON.hs
@@ -796,11 +796,13 @@
 withBoundedScientific_ :: (Parser a -> Parser a) -> (Scientific -> Parser a) -> Value -> Parser a
 withBoundedScientific_ whenFail f (Number scientific) =
     if exp10 > 1024
-    then whenFail (fail msg)
+    then whenFail (fail (msg "greater than 1024"))
+    else if exp10 < -1024
+    then whenFail (fail (msg "less than -1024"))
     else f scientific
   where
     exp10 = base10Exponent scientific
-    msg = "found a number with exponent " ++ show exp10 ++ ", but it must not be greater than 1024"
+    msg req = "found a number with exponent " ++ show exp10 ++ ", but it must not be " ++ req
 withBoundedScientific_ whenFail _ v =
     whenFail (typeMismatch "Number" v)
 
@@ -1706,14 +1708,7 @@
         _      -> Scientific.toRealFloat <$> parseScientificText t
 
 instance (FromJSON a, Integral a) => FromJSON (Ratio a) where
-    parseJSON (Number x)
-      | exp10 <= 1024
-      , exp10 >= -1024 = return $! realToFrac x
-      | otherwise      = prependContext "Ratio" $ fail msg
-      where
-        exp10 = base10Exponent x
-        msg = "found a number with exponent " ++ show exp10
-           ++ ", but it must not be greater than 1024 or less than -1024"
+    parseJSON n@(Number _) = withBoundedScientific "Ratio" (($!) pure . realToFrac) n
     parseJSON o = objParser o
       where
         objParser = withObject "Rational" $ \obj -> do
diff --git a/tests/DoubleToScientific.hs b/tests/DoubleToScientific.hs
--- a/tests/DoubleToScientific.hs
+++ b/tests/DoubleToScientific.hs
@@ -114,7 +114,7 @@
     (s, e, lower_boundary_is_closer) = decodeFloat' v
     lowerBoundaryCloser' = if lower_boundary_is_closer then lowerBoundaryCloser else id
 
--- | return significant, exponent and whether lower boundery is closer.
+-- | return significand, exponent and whether lower boundary is closer.
 --
 -- GHC's decodeFloat does "weird" stuff to denormal doubles,
 -- that messes up our delta calculation.
diff --git a/tests/JSONTestSuite/results/n_array_spaces_vertical_tab_formfeed.tok b/tests/JSONTestSuite/results/n_array_spaces_vertical_tab_formfeed.tok
--- a/tests/JSONTestSuite/results/n_array_spaces_vertical_tab_formfeed.tok
+++ b/tests/JSONTestSuite/results/n_array_spaces_vertical_tab_formfeed.tok
@@ -1,3 +1,3 @@
 TkArrayOpen
 TkItem
-TkErr "Unespected control character while parsing string literal"
+TkErr "Unexpected control character while parsing string literal"
diff --git a/tests/JSONTestSuite/results/n_string_unescaped_ctrl_char.tok b/tests/JSONTestSuite/results/n_string_unescaped_ctrl_char.tok
--- a/tests/JSONTestSuite/results/n_string_unescaped_ctrl_char.tok
+++ b/tests/JSONTestSuite/results/n_string_unescaped_ctrl_char.tok
@@ -1,3 +1,3 @@
 TkArrayOpen
 TkItem
-TkErr "Unespected control character while parsing string literal"
+TkErr "Unexpected control character while parsing string literal"
diff --git a/tests/JSONTestSuite/results/n_string_unescaped_newline.tok b/tests/JSONTestSuite/results/n_string_unescaped_newline.tok
--- a/tests/JSONTestSuite/results/n_string_unescaped_newline.tok
+++ b/tests/JSONTestSuite/results/n_string_unescaped_newline.tok
@@ -1,3 +1,3 @@
 TkArrayOpen
 TkItem
-TkErr "Unespected control character while parsing string literal"
+TkErr "Unexpected control character while parsing string literal"
diff --git a/tests/JSONTestSuite/results/n_string_unescaped_tab.tok b/tests/JSONTestSuite/results/n_string_unescaped_tab.tok
--- a/tests/JSONTestSuite/results/n_string_unescaped_tab.tok
+++ b/tests/JSONTestSuite/results/n_string_unescaped_tab.tok
@@ -1,3 +1,3 @@
 TkArrayOpen
 TkItem
-TkErr "Unespected control character while parsing string literal"
+TkErr "Unexpected control character while parsing string literal"
diff --git a/tests/UnitTests.hs b/tests/UnitTests.hs
--- a/tests/UnitTests.hs
+++ b/tests/UnitTests.hs
@@ -38,6 +38,7 @@
   , Value(..), camelTo, camelTo2
   , defaultOptions, formatPath, formatRelativePath, omitNothingFields, parse)
 import Data.Char (toUpper, GeneralCategory(Control,Surrogate), generalCategory)
+import Data.Fixed (Nano)
 import Data.HashMap.Strict (HashMap)
 import Data.Kind (Type)
 import Data.List (isSuffixOf)
@@ -87,7 +88,8 @@
   _    -> s -- shouldn't happen?
   where
     split c' s' = map L.unpack $ L.split c' $ L.pack s'
-    capitalize t = toUpper (head t) : tail t
+    capitalize []     = []
+    capitalize (x:xs) = toUpper x : xs
 
 
 data Wibble = Wibble {
@@ -458,17 +460,22 @@
     (eitherDecode "1.37" :: Either String Rational)
 
 bigRationalDecoding :: Assertion
-bigRationalDecoding =
-  assertEqual "Decoding an Integer with a large exponent should fail"
-    (Left "Error in $: parsing Ratio failed, found a number with exponent 2000, but it must not be greater than 1024 or less than -1024")
+bigRationalDecoding = do
+  assertEqual "Decoding a Rational with a large exponent should fail"
+    (Left "Error in $: parsing Ratio failed, found a number with exponent 2000, but it must not be greater than 1024")
     ((eitherDecode :: L.ByteString -> Either String Rational) "1e2000")
-
-smallRationalDecoding :: Assertion
-smallRationalDecoding =
-  assertEqual "Decoding an Integer with a large exponent should fail"
-    (Left "Error in $: parsing Ratio failed, found a number with exponent -2000, but it must not be greater than 1024 or less than -1024")
+  assertEqual "Decoding a Rational with a small exponent should fail"
+    (Left "Error in $: parsing Ratio failed, found a number with exponent -2000, but it must not be less than -1024")
     ((eitherDecode :: L.ByteString -> Either String Rational) "1e-2000")
 
+bigFixedDecoding :: Assertion
+bigFixedDecoding = do
+  assertEqual "Decoding a Fixed with a large exponent should fail"
+    (Left "Error in $: parsing Fixed failed, found a number with exponent 9999, but it must not be greater than 1024")
+    ((eitherDecode :: L.ByteString -> Either String Nano) "1e9999")
+  assertEqual "Decoding a Fixed with a small exponent should fail"
+    (Left "Error in $: parsing Fixed failed, found a number with exponent -9999, but it must not be less than -1024")
+    ((eitherDecode :: L.ByteString -> Either String Nano) "1e-9999")
 
 bigScientificExponent :: Assertion
 bigScientificExponent =
@@ -555,7 +562,7 @@
   , testCase "Ratio with denominator 0" ratioDenominator0
   , testCase "Rational parses number"   rationalNumber
   , testCase "Big rational"             bigRationalDecoding
-  , testCase "Small rational"           smallRationalDecoding
+  , testCase "Big fixed"                bigFixedDecoding
   , testCase "Big scientific exponent" bigScientificExponent
   , testCase "Big integer decoding" bigIntegerDecoding
   , testCase "Big natural decoding" bigNaturalDecoding
