diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
 # Change Log
 
+## [0.0.2.3] - 2023-11-08
+
+- Exposed `Codec.Tiled.World.IO`.
+- Made `propertytype` optional.
+
 ## [0.0.2.2] - 2022-12-24
 
 - Fixed bits and flags in Data.Tiled.GID.
@@ -27,6 +32,8 @@
 
 Initial release.
 
+[0.0.2.3]: https://github.com/haskell-game/aeson-tiled/releases/tag/v0.0.2.3
+[0.0.2.2]: https://github.com/haskell-game/aeson-tiled/releases/tag/v0.0.2.2
 [0.0.2.1]: https://github.com/haskell-game/aeson-tiled/releases/tag/v0.0.2.1
 [0.0.2.0]: https://github.com/haskell-game/aeson-tiled/releases/tag/v0.0.2.0
 [0.0.1.0]: https://github.com/haskell-game/aeson-tiled/releases/tag/v0.0.1.0
diff --git a/aeson-tiled.cabal b/aeson-tiled.cabal
--- a/aeson-tiled.cabal
+++ b/aeson-tiled.cabal
@@ -1,6 +1,6 @@
 cabal-version: 3.0
 name: aeson-tiled
-version: 0.0.2.2
+version: 0.0.2.3
 synopsis: Aeson instances for the Tiled map editor.
 description:
   The mighty Tiled 2D map editor is an open source
@@ -16,10 +16,11 @@
 build-type: Simple
 
 extra-source-files:
-  README.md,
-  ChangeLog.md,
   maps/microgue/*.tmj,
   maps/microgue/*.tsj
+extra-doc-files:
+  README.md,
+  ChangeLog.md
 
 library
   hs-source-dirs: src
@@ -50,6 +51,7 @@
     Codec.Tiled.Tileset.WangSet
     Codec.Tiled.Tileset.WangTile
     Codec.Tiled.World
+    Codec.Tiled.World.IO
     Codec.Tiled.World.Map
     Codec.Tiled.World.Pattern
     Data.Tiled.GID
@@ -108,3 +110,7 @@
     PatternSynonyms,
     RecordWildCards,
     StrictData,
+
+source-repository head
+    type:     git
+    location: https://github.com/haskell-game/aeson-tiled
diff --git a/src/Codec/Tiled/Property.hs b/src/Codec/Tiled/Property.hs
--- a/src/Codec/Tiled/Property.hs
+++ b/src/Codec/Tiled/Property.hs
@@ -9,7 +9,7 @@
 data Property = Property
   { name         :: Text       -- ^ Name of the property
   , type_        :: Maybe Text -- ^ Type of the property (@string@ (default), @int@, @float@, @bool@, @color@, @file@, @object@ or @class@)
-  , propertyType :: Text       -- ^ Name of the custom property type, when applicable
+  , propertyType :: Maybe Text -- ^ Name of the custom property type, when applicable
   , value        :: Value      -- ^ Value of the property
   }
   deriving (Eq, Show, Generic)
diff --git a/src/Codec/Tiled/World/IO.hs b/src/Codec/Tiled/World/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Codec/Tiled/World/IO.hs
@@ -0,0 +1,37 @@
+module Codec.Tiled.World.IO
+  ( WorldError(..)
+  , readFile
+  , writeFile
+  ) where
+
+import Prelude hiding (readFile, writeFile)
+
+import Control.Exception (Exception, throwIO)
+import Control.Monad.IO.Class (MonadIO(..))
+import Data.Aeson qualified as Aeson
+import Data.ByteString (ByteString)
+import Data.ByteString qualified as ByteString
+import Data.Text (Text)
+import Data.Text qualified as Text
+
+import Codec.Tiled.World (World)
+
+newtype WorldError = WorldError Text
+  deriving (Eq, Show)
+
+instance Exception WorldError
+
+readFile :: MonadIO m => FilePath -> m World
+readFile source = liftIO do
+  bytes <- ByteString.readFile source
+  case decodeMap bytes of
+    Left msg ->
+      throwIO $ WorldError (Text.pack msg)
+    Right res ->
+      pure res
+
+decodeMap :: ByteString -> Either String World
+decodeMap = Aeson.eitherDecodeStrict'
+
+writeFile :: MonadIO m => FilePath -> World -> m ()
+writeFile destination = liftIO . Aeson.encodeFile destination
diff --git a/tests/Main.hs b/tests/Main.hs
--- a/tests/Main.hs
+++ b/tests/Main.hs
@@ -24,6 +24,8 @@
       [ testCase "Embedded tileset" mapTestRoundtripEmbedded
       , testCase "External tileset" mapTestRoundtripExternal
       ]
+  , testGroup "Custom properties"
+      [ testCase "Property name" mapTestPropertyName ]
   ]
 
 mapTestRoundtripEmbedded :: IO ()
@@ -43,6 +45,13 @@
     Map.writeFile out embeddedZstd
     embeddedZstdOut <- Map.readFile out
     embeddedZstd @?= embeddedZstdOut
+
+mapTestPropertyName :: IO ()
+mapTestPropertyName = do
+  testMap <- Map.readFile "maps/property-name.tmj"
+  print testMap
+    -- This will fail with “key "propertytype" not found” or
+    -- similar if aeson is not able to parse it correctly.
 
 tilesetCodecTests :: TestTree
 tilesetCodecTests = testGroup "Tileset"
