diff --git a/main.cpp b/main.cpp
index 1f70eac1836949b320a1c01d7f8baa0eb16126db..ed8c3307699c64dc2ed19860d54de7739cd88e4e 100644
--- a/main.cpp
+++ b/main.cpp
@@ -1,10 +1,12 @@
 #include <iostream>
 #include <fstream>
 #include <numeric>
+#include <vector>
 
 #include "gtest/gtest.h"
 
 #include "cell_tree.hpp"
+#include "swcio.hpp"
 #include "json/src/json.hpp"
 
 using json = nlohmann::json;
@@ -250,6 +252,179 @@ TEST(cell_tree, json_load) {
     }
 }
 
+// SWC tests
+void expect_cell_equals(const neuron::io::cell_record &expected,
+                        const neuron::io::cell_record &actual)
+{
+    EXPECT_EQ(expected.id(), actual.id());
+    EXPECT_EQ(expected.type(), actual.type());
+    EXPECT_FLOAT_EQ(expected.x(), actual.x());
+    EXPECT_FLOAT_EQ(expected.y(), actual.y());
+    EXPECT_FLOAT_EQ(expected.z(), actual.z());
+    EXPECT_FLOAT_EQ(expected.radius(), actual.radius());
+    EXPECT_EQ(expected.parent(), actual.parent());
+}
+
+TEST(cell_record, construction)
+{
+    using namespace neuron::io;
+
+    {
+        // force an invalid type
+        cell_record::kind invalid_type = static_cast<cell_record::kind>(100);
+        EXPECT_THROW(cell_record cell(invalid_type, 7, 1., 1., 1., 1., 5),
+                     std::invalid_argument);
+    }
+
+    {
+        // invalid id
+        EXPECT_THROW(cell_record cell(
+                         cell_record::custom, -3, 1., 1., 1., 1., 5),
+                     std::invalid_argument);
+    }
+
+    {
+        // invalid parent id
+        EXPECT_THROW(cell_record cell(
+                         cell_record::custom, 0, 1., 1., 1., 1., -5),
+                     std::invalid_argument);
+    }
+
+    {
+        // invalid radius
+        EXPECT_THROW(cell_record cell(
+                         cell_record::custom, 0, 1., 1., 1., -1., -1),
+                     std::invalid_argument);
+    }
+
+    {
+        // parent_id > id
+        EXPECT_THROW(cell_record cell(
+                         cell_record::custom, 0, 1., 1., 1., 1., 2),
+                     std::invalid_argument);
+    }
+
+    {
+        // parent_id == id
+        EXPECT_THROW(cell_record cell(
+                         cell_record::custom, 0, 1., 1., 1., 1., 0),
+                     std::invalid_argument);
+    }
+
+    {
+        // check standard construction by value
+        cell_record cell(cell_record::custom, 0, 1., 1., 1., 1., -1);
+        EXPECT_EQ(cell.id(), 0);
+        EXPECT_EQ(cell.type(), cell_record::custom);
+        EXPECT_EQ(cell.x(), 1.);
+        EXPECT_EQ(cell.y(), 1.);
+        EXPECT_EQ(cell.z(), 1.);
+        EXPECT_EQ(cell.radius(), 1.);
+        EXPECT_EQ(cell.diameter(), 2*1.);
+        EXPECT_EQ(cell.parent(), -1);
+    }
+
+    {
+        // check copy constructor
+        cell_record cell_orig(cell_record::custom, 0, 1., 1., 1., 1., -1);
+        cell_record cell(cell_orig);
+        expect_cell_equals(cell_orig, cell);
+    }
+}
+
+TEST(swc_parser, invalid_input)
+{
+    using namespace neuron::io;
+
+    {
+        // check incomplete lines; missing parent
+        std::istringstream is("1 1 14.566132 34.873772 7.857000 0.717830\n");
+        cell_record cell;
+        EXPECT_THROW(is >> cell, std::logic_error);
+    }
+
+    {
+        // Check non-parsable values
+        std::istringstream is("1a 1 14.566132 34.873772 7.857000 0.717830 -1\n");
+        cell_record cell;
+        EXPECT_THROW(is >> cell, std::logic_error);
+    }
+
+    {
+        // Check invalid cell type
+        std::istringstream is("1 10 14.566132 34.873772 7.857000 0.717830 -1\n");
+        cell_record cell;
+        EXPECT_THROW(is >> cell, std::invalid_argument);
+    }
+}
+
+
+TEST(swc_parser, valid_input)
+{
+    using namespace neuron::io;
+
+    {
+        // check empty file; no cell may be parsed
+        cell_record cell, cell_orig;
+        std::istringstream is("");
+        EXPECT_NO_THROW(is >> cell);
+        expect_cell_equals(cell_orig, cell);
+    }
+
+    {
+        // check comment-only file not ending with a newline;
+        // no cell may be parsed
+        cell_record cell, cell_orig;
+        std::istringstream is("#comment\n#comment");
+        EXPECT_NO_THROW(is >> cell);
+        expect_cell_equals(cell_orig, cell);
+    }
+
+
+    {
+        // check last line case (no newline at the end)
+        std::istringstream is("1 1 14.566132 34.873772 7.857000 0.717830 -1");
+        cell_record cell;
+        EXPECT_NO_THROW(is >> cell);
+        EXPECT_EQ(0, cell.id());    // zero-based indexing
+        EXPECT_EQ(cell_record::soma, cell.type());
+        EXPECT_FLOAT_EQ(14.566132, cell.x());
+        EXPECT_FLOAT_EQ(34.873772, cell.y());
+        EXPECT_FLOAT_EQ( 7.857000, cell.z());
+        EXPECT_FLOAT_EQ( 0.717830, cell.radius());
+        EXPECT_FLOAT_EQ( -1, cell.parent());
+    }
+
+    {
+        // check valid input with a series of records
+        std::vector<cell_record> cells_orig = {
+            cell_record(cell_record::soma, 0,
+                        14.566132, 34.873772, 7.857000, 0.717830, -1),
+            cell_record(cell_record::dendrite, 1,
+                        14.566132+1, 34.873772+1, 7.857000+1, 0.717830+1, -1)
+        };
+
+        std::stringstream swc_input;
+        swc_input << "# this is a comment\n";
+        swc_input << "# this is a comment\n";
+        for (auto c : cells_orig)
+            swc_input << c << "\n";
+
+        swc_input << "# this is a final comment\n";
+        try {
+            std::size_t nr_records = 0;
+            cell_record cell;
+            while ( !(swc_input >> cell).eof()) {
+                ASSERT_LT(nr_records, cells_orig.size());
+                expect_cell_equals(cells_orig[nr_records], cell);
+                ++nr_records;
+            }
+        } catch (std::exception &e) {
+            ADD_FAILURE();
+        }
+    }
+}
+
 int main(int argc, char **argv) {
     ::testing::InitGoogleTest(&argc, argv);
     return RUN_ALL_TESTS();
diff --git a/makefile b/makefile
index 3a75e5b6907eb4c1efd25d95d5ed8e521d911831..736a149f83bcb53f082a9a3656d7f1ef5ca5f321 100644
--- a/makefile
+++ b/makefile
@@ -1,5 +1,5 @@
 CC=clang++
-FLAGS=-std=c++11 -g
+FLAGS=-std=c++11 -g -pedantic
 
 test.exe : main.cpp *.hpp makefile gtest.o
 	$(CC) $(FLAGS) main.cpp -o test.exe gtest.o -pthread
diff --git a/swcio.hpp b/swcio.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d362109dd15b771ac897bb884380682214f113b0
--- /dev/null
+++ b/swcio.hpp
@@ -0,0 +1,263 @@
+#pragma once
+
+#include <exception>
+#include <iostream>
+#include <sstream>
+#include <type_traits>
+
+namespace neuron
+{
+
+namespace io
+{
+
+
+static bool starts_with(const std::string &str, const std::string &prefix)
+{
+    return (str.find(prefix) == 0);
+}
+
+class cell_record 
+{
+public:
+
+    // FIXME: enum's are not completely type-safe, since they can accept anything
+    // that can be casted to their underlying type.
+    // 
+    // More on SWC files: http://research.mssm.edu/cnic/swc.html
+    enum kind {
+        undefined = 0,
+        soma,
+        axon,
+        dendrite,
+        apical_dendrite,
+        fork_point,
+        end_point,
+        custom
+    };
+
+    // cell records assume zero-based indexing; root's parent remains -1
+    cell_record(kind type, int id, 
+                float x, float y, float z, float r,
+                int parent_id)
+        : type_(type)
+        , id_(id)
+        , x_(x)
+        , y_(y)
+        , z_(z)
+        , r_(r)
+        , parent_id_(parent_id)
+    {
+        // Check cell type as well; enum's do not offer complete type safety,
+        // since you can cast anything that fits to its underlying type
+        if (type_ < 0 || type_ > custom)
+            throw std::invalid_argument("unknown cell type");
+
+        if (id_ < 0)
+            throw std::invalid_argument("negative ids not allowed");
+        
+        if (parent_id_ < -1)
+            throw std::invalid_argument("parent_id < -1 not allowed");
+
+        if (parent_id_ >= id_)
+            throw std::invalid_argument("parent_id >= id is not allowed");
+
+        if (r_ < 0)
+            throw std::invalid_argument("negative radii are not allowed");
+    }
+    
+    cell_record()
+        : type_(cell_record::undefined)
+        , id_(0)
+        , x_(0)
+        , y_(0)
+        , z_(0)
+        , r_(0)
+        , parent_id_(-1)
+    { }
+
+    cell_record(const cell_record &other) = default;
+    cell_record &operator=(const cell_record &other) = default;
+
+    friend bool operator==(const cell_record &lhs,
+                           const cell_record &rhs)
+    {
+        return lhs.id_ == rhs.id_;
+    }
+
+    friend bool operator!=(const cell_record &lhs,
+                           const cell_record &rhs)
+    {
+        return !(lhs == rhs);
+    }
+
+    friend std::ostream &operator<<(std::ostream &os, const cell_record &cell);
+
+    kind type() const
+    {
+        return type_;
+    }
+
+    int id() const
+    {
+        return id_;
+    }
+
+    int parent() const
+    {
+        return parent_id_;
+    }
+
+    float x() const
+    {
+        return x_;
+    }
+
+    float y() const
+    {
+        return y_;
+    }
+
+    float z() const
+    {
+        return z_;
+    }
+
+    float radius() const
+    {
+        return r_;
+    }
+
+    float diameter() const
+    {
+        return 2*r_;
+    }
+
+private:
+    kind type_;         // cell type
+    int id_;            // cell id
+    float x_, y_, z_;   // cell coordinates
+    float r_;           // cell radius
+    int parent_id_;     // cell parent's id
+};
+
+class swc_parser
+{
+public:
+    swc_parser(const std::string &delim,
+               std::string comment_prefix)
+        : delim_(delim)
+        , comment_prefix_(comment_prefix)
+    { }
+
+    swc_parser()
+        : delim_(" ")
+        , comment_prefix_("#")
+    { }
+
+    std::istream &parse_record(std::istream &is, cell_record &cell)
+    {
+        while (!is.eof() && !is.bad()) {
+            // consume empty and comment lines first
+            std::getline(is, linebuff_);
+            if (!linebuff_.empty() && !starts_with(linebuff_, comment_prefix_))
+                break;
+        }
+
+        if (is.bad())
+            // let the caller check for such events
+            return is;
+
+        if (is.eof() &&
+            (linebuff_.empty() || starts_with(linebuff_, comment_prefix_)))
+            // last line is either empty or a comment; don't parse anything
+            return is;
+
+        if (is.fail())
+            throw std::runtime_error("too long line detected");
+
+        std::istringstream line(linebuff_);
+        cell = parse_record(line);
+        return is;
+    }
+
+private:
+    void check_parse_status(const std::istream &is)
+    {
+        if (is.fail())
+            // If we try to read past the eof; fail bit will also be set
+            // FIXME: better throw a custom parse_error exception
+            throw std::logic_error("could not parse value");
+    }
+
+    template<typename T>
+    T parse_value_strict(std::istream &is)
+    {
+        T val;
+        check_parse_status(is >> val);
+
+        // everything's fine
+        return val;
+    }
+
+    // Read the record from a string stream; will be treated like a single line
+    cell_record parse_record(std::istringstream &is);
+
+    std::string delim_;
+    std::string comment_prefix_;
+    std::string linebuff_;
+};
+
+
+// specialize parsing for cell types
+template<>
+cell_record::kind swc_parser::parse_value_strict(std::istream &is)
+{
+    int val;
+    check_parse_status(is >> val);
+
+    // Let cell_record's constructor check for the type validity
+    return static_cast<cell_record::kind>(val);
+}
+
+cell_record swc_parser::parse_record(std::istringstream &is)
+{
+    auto id = parse_value_strict<int>(is);
+    auto type = parse_value_strict<cell_record::kind>(is);
+    auto x = parse_value_strict<float>(is);
+    auto y = parse_value_strict<float>(is);
+    auto z = parse_value_strict<float>(is);
+    auto r = parse_value_strict<float>(is);
+    auto parent_id = parse_value_strict<int>(is);
+
+    // Convert to zero-based, leaving parent_id as-is if -1
+    if (parent_id != -1)
+        parent_id--;
+
+    return cell_record(type, id-1, x, y, z, r, parent_id);
+}
+
+
+std::istream &operator>>(std::istream &is, cell_record &cell)
+{
+    swc_parser parser;
+    parser.parse_record(is, cell);
+    return is;
+}
+
+std::ostream &operator<<(std::ostream &os, const cell_record &cell)
+{
+    // output in one-based indexing
+    os << cell.id_+1 << " "
+       << cell.type_ << " "
+       << std::setprecision(7) << cell.x_    << " "
+       << std::setprecision(7) << cell.y_    << " "
+       << std::setprecision(7) << cell.z_    << " "
+       << std::setprecision(7) << cell.r_    << " "
+       << ((cell.parent_id_ == -1) ? cell.parent_id_ : cell.parent_id_+1);
+
+    return os;
+}
+
+}   // end of neuron::io
+}   // end of neuron