diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for javelin-io
+
+## Release 0.1.1.0
+
+* This is the first version of `javelin-io`.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) Laurent P. René de Cotret
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/javelin-io.cabal b/javelin-io.cabal
new file mode 100644
--- /dev/null
+++ b/javelin-io.cabal
@@ -0,0 +1,71 @@
+cabal-version:      3.0
+name:               javelin-io
+version:            0.1.1.0
+synopsis:           IO operations for the `javelin` package
+license:            MIT
+license-file:       LICENSE
+author:             Laurent P. René de Cotret
+maintainer:         laurent.decotret@outlook.com
+category:           Data, Data Structures, Data Science
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+extra-source-files: test/data/*.csv
+tested-with:        GHC ==9.8.1 
+                     || ==9.6.3
+                     || ==9.4.7
+description:
+        
+        This package implements serialization/deserialization of 'Series', labeled one-dimensional arrays
+        combining properties from maps and arrays.
+        
+        The important modules are:
+        
+        ["Data.Series.IO"] Serialization/deserialization of series of arbitrary types.
+        
+        ["Data.Series.Unboxed.IO"] Serialization/deserialization of unboxed series for better performance, at the cost of flexibility.
+        
+        ["Data.Series.Generic.IO"] Serialization/deserialization of generic series to manipulate any type of 'Series'.
+        
+        If you don't know where to start, please take a look at the documentation in "Data.Series.IO".
+
+common common
+    default-language: GHC2021
+    ghc-options: -Wall
+                 -Wcompat
+                 -Widentities
+                 -Wincomplete-uni-patterns
+                 -Wincomplete-record-updates
+                 -Wredundant-constraints
+                 -fhide-source-paths
+                 -Wpartial-fields
+
+library
+    import:           common
+    hs-source-dirs:   src
+    exposed-modules:  Data.Series.IO
+                      Data.Series.Generic.IO
+                      Data.Series.Unboxed.IO
+    build-depends:    base >=4.15.0.0 && <4.20,
+                      bytestring ^>=0.12,
+                      cassava ^>=0.5,
+                      containers >=0.6 && <0.8,
+                      unordered-containers ^>=0.2,
+                      javelin ^>=0.1,
+                      vector >=0.12.3.0 && <0.14,
+
+test-suite javelin-io-test
+    import:           common
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          Main.hs
+    other-modules:    Test.Data.Series.IO
+    build-depends:    base,
+                      cassava,
+                      filepath,
+                      javelin,
+                      javelin-io,
+                      tasty,
+                      tasty-hedgehog,
+                      tasty-hunit,
+                      temporary,
+                      vector
diff --git a/src/Data/Series/Generic/IO.hs b/src/Data/Series/Generic/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Series/Generic/IO.hs
@@ -0,0 +1,218 @@
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE UndecidableInstances #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  $module
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT
+-- Maintainer  :  laurent.decotret@outlook.com
+-- Portability :  portable
+--
+-- This module contains functions to serialize/deserialize generic 'Series'
+-- to/from bytes.
+--
+-- Use this module if you want to support all types of 'Series'. Otherwise,
+-- you should use either the modules "Data.Series.IO" or "Data.Series.Unboxed.IO". 
+module Data.Series.Generic.IO (
+    -- * Deserialize 'Series'
+    readCSV,
+    readCSVFromFile,
+
+    -- * Serialize 'Series'
+    writeCSV,
+    writeCSVToFile,
+) where
+
+
+import           Control.Monad          ( forM )
+import           Control.Monad.IO.Class ( MonadIO(liftIO) )
+import qualified Data.ByteString        as BS
+import qualified Data.ByteString.Lazy   as BL
+import           Data.Csv               ( FromNamedRecord(..), ToNamedRecord(..), )
+import qualified Data.Csv               as CSV
+import           Data.Functor           ( (<&>) )
+import qualified Data.HashMap.Strict    as HashMap
+import qualified Data.List.NonEmpty     as NE
+import           Data.Maybe             ( fromMaybe )
+import           Data.Series.Generic    ( Series, fromVector, convert )
+import qualified Data.Series.Generic    as GSeries
+import qualified Data.Vector            as Boxed
+import           Data.Vector.Generic    ( Vector )
+import qualified System.IO              as IO
+
+
+{-|
+Read a comma-separated value (CSV) bytestream into a series.
+
+Consider the following bytestream read from a file:
+
+@
+latitude,longitude,city
+48.856667,2.352222,Paris
+40.712778,-74.006111,New York City
+25.0375,121.5625,Taipei
+-34.603333,-58.381667,Buenos Aires
+@
+
+We want to get a series of the latitude an longitude, indexed by the column "city". First, we need
+to do is to create a datatype representing the latitude and longitude information, and our index:
+
+@
+data LatLong = MkLatLong { latitude  :: Double
+                         , longitude :: Double
+                         }
+    deriving ( Show )
+
+newtype City = MkCity String
+    deriving ( Eq, Ord, Show )
+@
+
+Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:
+
+@
+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )
+
+instance 'FromNamedRecord' LatLong where
+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"
+                                   \<*\> r .: "longitude"
+
+
+instance 'FromNamedRecord' City where
+    'parseNamedRecord' r = MkCity \<$\> r .: "city"
+@
+
+Finally, we're ready to read our stream:
+
+@
+import "Data.Series.Generic"
+import "Data.Series.Generic.IO"
+import "Data.Vector" 
+
+main :: IO ()
+main = do
+    stream <- (...) -- Read the bytestring from somewhere
+    let (latlongs  :: 'Series' Vector City LatLong) = either (error . show) id \<$\> `readCSV` stream
+    print latlongs
+@
+-}
+readCSV :: (Vector v a, Ord k, FromNamedRecord k, FromNamedRecord a)
+        => BL.ByteString
+        -> Either String (Series v k a)
+readCSV bytes = do
+    (_, records :: Boxed.Vector CSV.NamedRecord) <- CSV.decodeByName bytes
+
+    rows <- CSV.runParser $ forM records
+                          $ \record -> (,) <$> parseNamedRecord record
+                                           <*> parseNamedRecord record
+
+    pure $ convert $ fromVector rows
+
+
+fromFile :: MonadIO m 
+         => FilePath
+         -> (BL.ByteString -> Either String b)
+         -> m (Either String b)
+fromFile fp f
+    = liftIO $ IO.withFile fp IO.ReadMode $ \h -> do
+        IO.hSetBinaryMode h True
+        BS.hGetContents h <&> f . BL.fromStrict
+
+
+{-|
+This is a helper function to read a CSV directly from a filepath.
+See the documentation for 'readCSV' on how to prepare your types.
+Then, for example, you can use 'readCSVFromFile' as:
+
+@
+import "Data.Series.Generic"
+import "Data.Series.Generic.IO"
+import "Data.Vector" 
+
+main :: IO ()
+main = do
+    stream <- (...) -- Read the bytestring from somewhere
+    let (latlongs  :: 'Series' Vector City LatLong) = either (error . show) id \<$\> `readCSV` stream
+    print latlongs
+@
+-}
+readCSVFromFile :: (MonadIO m, Vector v a, Ord k, FromNamedRecord k, FromNamedRecord a)
+                => FilePath
+                -> m (Either String (Series v k a))
+readCSVFromFile fp = fromFile fp readCSV 
+
+
+
+{-|
+Read a comma-separated value (CSV) bytestream into a series.
+
+Consider the following bytestream read from a file:
+
+@
+latitude,longitude,city
+48.856667,2.352222,Paris
+40.712778,-74.006111,New York City
+25.0375,121.5625,Taipei
+-34.603333,-58.381667,Buenos Aires
+@
+
+We want to get a series of the latitude an longitude, indexed by the column "city". First, we need
+to do is to create a datatype representing the latitude and longitude information, and our index:
+
+@
+data LatLong = MkLatLong { latitude  :: Double
+                         , longitude :: Double
+                         }
+    deriving ( Show )
+
+newtype City = MkCity String
+    deriving ( Eq, Ord, Show )
+@
+
+Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:
+
+@
+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )
+
+instance 'FromNamedRecord' LatLong where
+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"
+                                   \<*\> r .: "longitude"
+
+
+instance 'FromNamedRecord' City where
+    'parseNamedRecord' r = MkCity \<$\> r .: "city"
+@
+
+Finally, we're ready to read our stream:
+
+@
+import Data.Series
+import Data.Series.IO
+
+main :: IO ()
+main = do
+    stream <- (...) -- Read the bytestring from somewhere
+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSV` stream
+    print latlongs
+@
+-}
+writeCSV :: (Vector v a, ToNamedRecord k, ToNamedRecord a)
+         => Series v k a
+         -> BL.ByteString
+writeCSV xs = fromMaybe mempty $ do
+    recs   <- NE.nonEmpty [ toNamedRecord k <> toNamedRecord v | (k, v) <- GSeries.toList xs]
+    let header =  CSV.header $ HashMap.keys $ NE.head recs
+    pure $ CSV.encodeByName header $ NE.toList recs
+
+
+-- | This is a helper function to write a 'Series' directly to CSV.
+-- See the documentation for 'writeCSV' on how to prepare your types.
+writeCSVToFile :: (MonadIO m, Vector v a, ToNamedRecord k, ToNamedRecord a)
+               => FilePath
+               -> Series v k a
+               -> m ()
+writeCSVToFile fp 
+    = liftIO 
+    . BS.writeFile fp 
+    . BL.toStrict 
+    . writeCSV
diff --git a/src/Data/Series/IO.hs b/src/Data/Series/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Series/IO.hs
@@ -0,0 +1,179 @@
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE UndecidableInstances #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  $module
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT
+-- Maintainer  :  laurent.decotret@outlook.com
+-- Portability :  portable
+--
+-- This module contains functions to serialize/deserialize 'Series'
+-- to/from bytes.
+module Data.Series.IO (
+    -- * Deserialize 'Series'
+    readCSV,
+    readCSVFromFile,
+
+    -- * Serialize 'Series'
+    writeCSV,
+    writeCSVToFile,
+) where
+
+
+import           Control.Monad.IO.Class ( MonadIO )
+import qualified Data.ByteString.Lazy   as BL
+import           Data.Csv               ( FromNamedRecord(..), ToNamedRecord(..), )
+import           Data.Series            ( Series )
+import qualified Data.Series.Generic.IO as Generic.IO
+
+
+{-|
+Read a comma-separated value (CSV) bytestream into a series.
+
+Consider the following bytestream read from a file:
+
+@
+latitude,longitude,city
+48.856667,2.352222,Paris
+40.712778,-74.006111,New York City
+25.0375,121.5625,Taipei
+-34.603333,-58.381667,Buenos Aires
+@
+
+We want to get a series of the latitude an longitude, indexed by the column "city". First, we need
+to do is to create a datatype representing the latitude and longitude information, and our index:
+
+@
+data LatLong = MkLatLong { latitude  :: Double
+                         , longitude :: Double
+                         }
+    deriving ( Show )
+
+newtype City = MkCity String
+    deriving ( Eq, Ord, Show )
+@
+
+Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:
+
+@
+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )
+
+instance 'FromNamedRecord' LatLong where
+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"
+                                   \<*\> r .: "longitude"
+
+
+instance 'FromNamedRecord' City where
+    'parseNamedRecord' r = MkCity \<$\> r .: "city"
+@
+
+Finally, we're ready to read our stream:
+
+@
+import "Data.Series"
+import "Data.Series.IO"
+
+main :: IO ()
+main = do
+    let fp = "path/to/my/file.csv"
+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSVFromFile` fp
+    print latlongs
+@
+-}
+readCSV :: (Ord k, FromNamedRecord k, FromNamedRecord a)
+        => BL.ByteString
+        -> Either String (Series k a)
+readCSV = Generic.IO.readCSV
+
+
+{-|
+This is a helper function to read a CSV directly from a filepath.
+See the documentation for 'readCSV' on how to prepare your types.
+Then, for example, you can use 'readCSVFromFile' as:
+
+@
+import "Data.Series"
+import "Data.Series.IO"
+
+main :: IO ()
+main = do
+    let fp = "path/to/my/file.csv"
+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSVFromFile` fp
+    print latlongs
+@
+-}
+readCSVFromFile :: (MonadIO m, Ord k, FromNamedRecord k, FromNamedRecord a)
+                => FilePath
+                -> m (Either String (Series k a))
+readCSVFromFile = Generic.IO.readCSVFromFile
+
+
+
+{-|
+Read a comma-separated value (CSV) bytestream into a series.
+
+Consider the following bytestream read from a file:
+
+@
+latitude,longitude,city
+48.856667,2.352222,Paris
+40.712778,-74.006111,New York City
+25.0375,121.5625,Taipei
+-34.603333,-58.381667,Buenos Aires
+@
+
+We want to get a series of the latitude an longitude, indexed by the column "city". First, we need
+to do is to create a datatype representing the latitude and longitude information, and our index:
+
+@
+data LatLong = MkLatLong { latitude  :: Double
+                         , longitude :: Double
+                         }
+    deriving ( Show )
+
+newtype City = MkCity String
+    deriving ( Eq, Ord, Show )
+@
+
+Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:
+
+@
+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )
+
+instance 'FromNamedRecord' LatLong where
+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"
+                                   \<*\> r .: "longitude"
+
+
+instance 'FromNamedRecord' City where
+    'parseNamedRecord' r = MkCity \<$\> r .: "city"
+@
+
+Finally, we're ready to read our stream:
+
+@
+import Data.Series
+import Data.Series.IO
+
+main :: IO ()
+main = do
+    stream <- (...) -- Read the bytestring from somewhere
+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSV` stream
+    print latlongs
+@
+-}
+writeCSV :: (ToNamedRecord k, ToNamedRecord a)
+         => Series k a
+         -> BL.ByteString
+writeCSV = Generic.IO.writeCSV
+
+
+-- | This is a helper function to write a 'Series' directly to CSV.
+-- See the documentation for 'writeCSV' on how to prepare your types.
+writeCSVToFile :: (MonadIO m, ToNamedRecord k, ToNamedRecord a)
+               => FilePath
+               -> Series k a
+               -> m ()
+writeCSVToFile = Generic.IO.writeCSVToFile
diff --git a/src/Data/Series/Unboxed/IO.hs b/src/Data/Series/Unboxed/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/Series/Unboxed/IO.hs
@@ -0,0 +1,199 @@
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE UndecidableInstances #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      :  $module
+-- Copyright   :  (c) Laurent P. René de Cotret
+-- License     :  MIT
+-- Maintainer  :  laurent.decotret@outlook.com
+-- Portability :  portable
+--
+-- This module contains functions to serialize/deserialize unboxed 'Series'
+-- to/from bytes.
+--
+-- = Why use unboxed series?
+--
+-- Unboxed series can have much better performance, at the cost of less flexibility. For example,
+-- an unboxed series cannot contain values of type @`Maybe` a@. Moreover, unboxed series aren't instances of 
+-- `Functor` or `Foldable`.
+--
+-- If you are hesitating, you should prefer the series implementation in the "Data.Series" module (and therefore
+-- the IO module "Data.Series.IO").
+module Data.Series.Unboxed.IO (
+    -- * Deserialize 'Series'
+    readCSV,
+    readCSVFromFile,
+
+    -- * Serialize 'Series'
+    writeCSV,
+    writeCSVToFile,
+) where
+
+
+import           Control.Monad.IO.Class ( MonadIO )
+import qualified Data.ByteString.Lazy   as BL
+import           Data.Csv               ( FromNamedRecord(..), ToNamedRecord(..), )
+import           Data.Series.Unboxed    ( Series )
+import qualified Data.Series.Generic.IO as Generic.IO
+import           Data.Vector.Unboxed    ( Unbox )
+
+
+{-|
+Read a comma-separated value (CSV) bytestream into a series.
+
+Consider the following bytestream read from a file:
+
+@
+latitude,longitude,city
+48.856667,2.352222,Paris
+40.712778,-74.006111,New York City
+25.0375,121.5625,Taipei
+-34.603333,-58.381667,Buenos Aires
+@
+
+We want to get a series of the latitude an longitude, indexed by the column "city". First, we need
+to do is to create a datatype representing the latitude and longitude information, and our index:
+
+@
+import qualified "Data.Vector.Unboxed"         as U
+import qualified "Data.Vector.Unboxed.Mutable" as UM
+import qualified "Data.Vector.Generic"         as G
+import qualified "Data.Vector.Generic.Mutable" as GM
+
+newtype Latitude = MkLatitude Double
+    deriving (Show)
+
+-- Special code to ensure that Latitude is unboxed
+-- This is only required for unboxed 'Series'
+newtype instance UM.MVector s Latitude = MV_Latitude (UM.MVector s Int)
+newtype instance U.Vector Latitude = V_Latitude (U.Vector Int)
+deriving instance GM.MVector UM.MVector Latitude
+deriving instance G.Vector U.Vector Latitude 
+instance U.Unbox Latitude
+
+newtype City = MkCity String
+    deriving ( Eq, Ord, Show )
+@
+
+Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:
+
+@
+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )
+
+instance 'FromNamedRecord' Latitude where
+    'parseNamedRecord' r = MkLatitude \<$\> r .: "latitude"
+
+
+instance 'FromNamedRecord' City where
+    'parseNamedRecord' r = MkCity \<$\> r .: "city"
+@
+
+Finally, we're ready to read our stream:
+
+@
+import "Data.Series.Unboxed"
+import "Data.Series.Unboxed.IO"
+
+main :: IO ()
+main = do
+    stream <- (...) -- Read the bytestring from somewhere
+    let (latitudes  :: 'Series' City Latitude) = either (error . show) id \<$\> `readCSV` stream
+    print latitudes
+@
+-}
+readCSV :: (Ord k, FromNamedRecord k, FromNamedRecord a, Unbox a)
+        => BL.ByteString
+        -> Either String (Series k a)
+readCSV = Generic.IO.readCSV
+
+
+{-|
+This is a helper function to read a CSV directly from a filepath.
+See the documentation for 'readCSV' on how to prepare your types.
+Then, for example, you can use 'readCSVFromFile' as:
+
+@
+import "Data.Series.Unboxed"
+import "Data.Series.Unboxed.IO"
+
+main :: IO ()
+main = do
+    stream <- (...) -- Read the bytestring from somewhere
+    let (latitudes  :: 'Series' City Latitude) = either (error . show) id \<$\> `readCSV` stream
+    print latitudes
+@
+-}
+readCSVFromFile :: (MonadIO m, Ord k, FromNamedRecord k, FromNamedRecord a, Unbox a)
+                => FilePath
+                -> m (Either String (Series k a))
+readCSVFromFile = Generic.IO.readCSVFromFile
+
+
+
+{-|
+Read a comma-separated value (CSV) bytestream into a series.
+
+Consider the following bytestream read from a file:
+
+@
+latitude,longitude,city
+48.856667,2.352222,Paris
+40.712778,-74.006111,New York City
+25.0375,121.5625,Taipei
+-34.603333,-58.381667,Buenos Aires
+@
+
+We want to get a series of the latitude an longitude, indexed by the column "city". First, we need
+to do is to create a datatype representing the latitude and longitude information, and our index:
+
+@
+data LatLong = MkLatLong { latitude  :: Double
+                         , longitude :: Double
+                         }
+    deriving ( Show )
+
+newtype City = MkCity String
+    deriving ( Eq, Ord, Show )
+@
+
+Second, we need to create an instance of `Data.Csv.FromNamedRecord` for our new types:
+
+@
+import "Data.Csv" ( 'FromNamedRecord', '(.:)' )
+
+instance 'FromNamedRecord' LatLong where
+    'parseNamedRecord' r = MkLatLong \<$\> r .: "latitude"
+                                   \<*\> r .: "longitude"
+
+
+instance 'FromNamedRecord' City where
+    'parseNamedRecord' r = MkCity \<$\> r .: "city"
+@
+
+Finally, we're ready to read our stream:
+
+@
+import Data.Series
+import Data.Series.IO
+
+main :: IO ()
+main = do
+    stream <- (...) -- Read the bytestring from somewhere
+    let (latlongs  :: 'Series' City LatLong) = either (error . show) id \<$\> `readCSV` stream
+    print latlongs
+@
+-}
+writeCSV :: (ToNamedRecord k, ToNamedRecord a, Unbox a)
+         => Series k a
+         -> BL.ByteString
+writeCSV = Generic.IO.writeCSV
+
+
+-- | This is a helper function to write a 'Series' directly to CSV.
+-- See the documentation for 'writeCSV' on how to prepare your types.
+writeCSVToFile :: (MonadIO m, ToNamedRecord k, ToNamedRecord a, Unbox a)
+               => FilePath
+               -> Series k a
+               -> m ()
+writeCSVToFile = Generic.IO.writeCSVToFile
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,9 @@
+module Main (main) where
+
+import qualified Test.Data.Series.IO
+
+import           Test.Tasty ( defaultMain, testGroup )
+
+main :: IO ()
+main = defaultMain $ testGroup "Test suite" [ Test.Data.Series.IO.tests
+                                            ]
diff --git a/test/Test/Data/Series/IO.hs b/test/Test/Data/Series/IO.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Data/Series/IO.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Test.Data.Series.IO (tests) where
+
+import           Data.Csv             ( FromNamedRecord(..), (.:), ToNamedRecord(..), (.=), namedRecord )
+import           Data.Series.Generic  ( Series, at)
+import qualified Data.Series.Generic  as Series
+import           Data.Series.IO       ( readCSVFromFile, writeCSVToFile )
+import           Data.String          ( IsString )
+import           Data.Vector          ( Vector )
+
+import           System.FilePath      ( (</>) )
+import           System.IO.Temp       ( withSystemTempDirectory )
+
+import           Test.Tasty           ( testGroup, TestTree ) 
+import           Test.Tasty.HUnit     ( testCase, assertEqual )
+
+tests :: TestTree
+tests = testGroup "Data.Series.IO" [ testReadCSVFromFile
+                                   , testWriteCSVToFile
+                                   ]
+
+
+data LatLong = MkLatLong {lat :: Double, long :: Double} deriving (Eq, Show)
+
+instance FromNamedRecord LatLong where
+    parseNamedRecord r = MkLatLong <$> r .: "latitude"
+                                   <*> r .: "longitude"
+
+instance ToNamedRecord LatLong where
+    toNamedRecord (MkLatLong lat long) = namedRecord [ "latitude" .= lat
+                                                     , "longitude" .= long
+                                                     ]
+
+newtype City = MkCity String
+    deriving (Eq, Ord, IsString, Show) 
+
+instance FromNamedRecord City where
+    parseNamedRecord r = MkCity <$> r .: "city"
+
+instance ToNamedRecord City where
+    toNamedRecord (MkCity city) = namedRecord ["city" .= city]
+
+
+testReadCSVFromFile :: TestTree
+testReadCSVFromFile = testCase "Read CSV data" $ do
+    (latlongs  :: Series Vector City LatLong) <- either (error . show) id <$> readCSVFromFile "test/data/lat-long-city.csv"
+
+    let latitudes  = fmap lat latlongs
+        longitudes = fmap long latlongs
+
+    assertEqual mempty 4 (Series.length latitudes)
+    assertEqual mempty (Just 48.856667) (latitudes `at` "Paris") 
+    assertEqual mempty (Just 40.712778) (latitudes `at` "New York City") 
+    assertEqual mempty (Just 25.0375) (latitudes `at` "Taipei") 
+    assertEqual mempty (Just (-34.603333)) (latitudes `at` "Buenos Aires") 
+
+    assertEqual mempty 4 (Series.length longitudes)
+    assertEqual mempty (Just 2.352222) (longitudes `at` "Paris") 
+    assertEqual mempty (Just (-74.006111)) (longitudes `at` "New York City") 
+    assertEqual mempty (Just 121.5625) (longitudes `at` "Taipei") 
+    assertEqual mempty (Just (-58.381667)) (longitudes `at` "Buenos Aires") 
+
+
+testWriteCSVToFile :: TestTree
+testWriteCSVToFile = testCase "Write CSV data" $ do
+    (latlongs  :: Series Vector City LatLong) <- either (error . show) id <$> readCSVFromFile "test/data/lat-long-city.csv"
+    
+    (written :: Series Vector City LatLong) <- withSystemTempDirectory "test-javelin-op" $ \dirname -> do
+        writeCSVToFile (dirname </> "myseries.csv") latlongs
+
+        either (error . show) id <$> readCSVFromFile (dirname </> "myseries.csv")
+    
+    assertEqual mempty latlongs written
diff --git a/test/data/lat-long-city.csv b/test/data/lat-long-city.csv
new file mode 100644
--- /dev/null
+++ b/test/data/lat-long-city.csv
@@ -0,0 +1,5 @@
+latitude,longitude,city
+48.856667,2.352222,Paris
+40.712778,-74.006111,New York City
+25.0375,121.5625,Taipei
+-34.603333,-58.381667,Buenos Aires
