Repository: jtablesaw/tablesaw
Pull request #452 (2019-01-31 18:45:59)

Add JSON reader support
Thanks for contributing.

- [X] Tick to sign-off your agreement to the [Developer Certificate of Origin (DCO) 1.1](https://developercertificate.org)

## Description

Described in https://github.com/jtablesaw/tablesaw/issues/374. The tests demonstrate the functionality well

Created base class `ReadOptions` for `CsvReadOptions`  to decouple cell parsing from CSV reading
Refactored out `ColumnTypeDetector` from `CsvReader` so that it could be used in reading html and json

## Testing

Added unit tests for new JsonReader functionality

benmccann: @lwhite1 I pushed some more updates to this. I'm pretty happy with it, but thought I'd give you a chance to give it a look since it's a decent sized change. Let me know if you're busy and want me to just go ahead without a review

Patch:
diff --git a/core/pom.xml b/core/pom.xml
index f5dbbfe7..eb4c6189 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -90,6 +90,16 @@
             <artifactId>guava</artifactId>
             <version>25.1-jre</version>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.9.8</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.wnameless</groupId>
+            <artifactId>json-flattener</artifactId>
+            <version>0.6.0</version>
+        </dependency>
         <dependency>
             <groupId>com.univocity</groupId>
             <artifactId>univocity-parsers</artifactId>
diff --git a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
index 687ca3f1..301f23ea 100644
--- a/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/BooleanColumn.java
@@ -326,7 +326,11 @@ public class BooleanColumn extends AbstractColumn<Boolean> implements BooleanMap
 
     @Override
     public String getUnformattedString(int row) {
-        return String.valueOf(get(row));
+        Boolean b = get(row);
+        if (b == null) {
+            return "";
+        }
+        return String.valueOf(b);
     }
 
     @Override
diff --git a/core/src/main/java/tech/tablesaw/api/ColumnType.java b/core/src/main/java/tech/tablesaw/api/ColumnType.java
index b05eea9f..8d671773 100644
--- a/core/src/main/java/tech/tablesaw/api/ColumnType.java
+++ b/core/src/main/java/tech/tablesaw/api/ColumnType.java
@@ -15,7 +15,7 @@ import tech.tablesaw.columns.numbers.ShortColumnType;
 import tech.tablesaw.columns.strings.StringColumnType;
 import tech.tablesaw.columns.strings.TextColumnType;
 import tech.tablesaw.columns.times.TimeColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -64,7 +64,7 @@ public interface ColumnType {
 
     String getPrinterFriendlyName();
 
-    AbstractParser<?> customParser(CsvReadOptions options);
+    AbstractParser<?> customParser(ReadOptions options);
 
     default boolean compare(int rowNumber, Column<?> temp, Column<?> original) {
         return original.get(rowNumber).equals(temp.get(temp.size() - 1));
diff --git a/core/src/main/java/tech/tablesaw/api/Row.java b/core/src/main/java/tech/tablesaw/api/Row.java
index d1a91848..bc9db6fb 100644
--- a/core/src/main/java/tech/tablesaw/api/Row.java
+++ b/core/src/main/java/tech/tablesaw/api/Row.java
@@ -196,11 +196,11 @@ public class Row implements Iterator<Row> {
         return dateTimeColumnMap.get(columnNames[columnIndex]).getLongInternal(rowNumber);
     }
 
-    public boolean getBoolean(String columnName) {
+    public Boolean getBoolean(String columnName) {
         return booleanColumnMap.get(columnName).get(rowNumber);
     }
 
-    public boolean getBoolean(int columnIndex) {
+    public Boolean getBoolean(int columnIndex) {
         return getBoolean(columnNames[columnIndex]);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/api/ShortColumn.java b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
index cc0d1441..ed08ed5c 100644
--- a/core/src/main/java/tech/tablesaw/api/ShortColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/ShortColumn.java
@@ -10,10 +10,9 @@ import it.unimi.dsi.fastutil.shorts.ShortComparator;
 import it.unimi.dsi.fastutil.shorts.ShortListIterator;
 import it.unimi.dsi.fastutil.shorts.ShortOpenHashSet;
 import it.unimi.dsi.fastutil.shorts.ShortSet;
-import tech.tablesaw.columns.Column;
 import tech.tablesaw.columns.AbstractParser;
+import tech.tablesaw.columns.Column;
 import tech.tablesaw.columns.numbers.DoubleColumnType;
-import tech.tablesaw.columns.numbers.IntColumnType;
 import tech.tablesaw.columns.numbers.NumberColumnFormatter;
 import tech.tablesaw.columns.numbers.ShortColumnType;
 import tech.tablesaw.selection.Selection;
@@ -355,7 +354,7 @@ public class ShortColumn extends NumberColumn<Short> implements CategoricalColum
     @Override
     public String getUnformattedString(final int row) {
         final int value = getInt(row);
-        if (IntColumnType.isMissingValue(value)) {
+        if (ShortColumnType.isMissingValue(value)) {
             return "";
         }
         return String.valueOf(value);
diff --git a/core/src/main/java/tech/tablesaw/api/TimeColumn.java b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
index 3b26133f..30f1b81a 100644
--- a/core/src/main/java/tech/tablesaw/api/TimeColumn.java
+++ b/core/src/main/java/tech/tablesaw/api/TimeColumn.java
@@ -39,10 +39,8 @@ import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -504,15 +502,6 @@ public class TimeColumn extends AbstractColumn<LocalTime>
         return data.iterator();
     }
 
-    Set<LocalTime> asSet() {
-        Set<LocalTime> times = new HashSet<>();
-        TimeColumn unique = unique();
-        for (LocalTime t : unique) {
-            times.add(t);
-        }
-        return times;
-    }
-
     public boolean contains(LocalTime time) {
         int t = PackedLocalTime.pack(time);
         return data().contains(t);
diff --git a/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java b/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
index 7c8ec504..68e7bf51 100644
--- a/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/SkipColumnType.java
@@ -1,6 +1,6 @@
 package tech.tablesaw.columns;
 
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class SkipColumnType extends AbstractColumnType {
 
@@ -22,7 +22,7 @@ public class SkipColumnType extends AbstractColumnType {
     }
 
     @Override
-    public AbstractParser<?> customParser(CsvReadOptions options) {
+    public AbstractParser<?> customParser(ReadOptions options) {
         throw new UnsupportedOperationException("Column type " + name() + " doesn't support parsing");
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java
index 173b5575..47c5aaaa 100644
--- a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.booleans;
 import tech.tablesaw.api.BooleanColumn;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class BooleanColumnType extends AbstractColumnType {
 
@@ -36,7 +36,7 @@ public class BooleanColumnType extends AbstractColumnType {
     }
 
     @Override
-    public BooleanParser customParser(CsvReadOptions readOptions) {
+    public BooleanParser customParser(ReadOptions readOptions) {
         return new BooleanParser(this, readOptions);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanParser.java b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanParser.java
index f4544ba3..4e4aa762 100644
--- a/core/src/main/java/tech/tablesaw/columns/booleans/BooleanParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/booleans/BooleanParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.booleans;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.util.Arrays;
 import java.util.List;
@@ -34,7 +34,7 @@ public class BooleanParser extends AbstractParser<Boolean> {
         super(columnType);
     }
 
-    public BooleanParser(BooleanColumnType booleanColumnType, CsvReadOptions readOptions) {
+    public BooleanParser(BooleanColumnType booleanColumnType, ReadOptions readOptions) {
         super(booleanColumnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java b/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java
index a245c02c..975a326b 100644
--- a/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/dates/DateColumnType.java
@@ -4,7 +4,7 @@ import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.DateColumn;
 import tech.tablesaw.columns.AbstractColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalDate;
 
@@ -32,7 +32,7 @@ public class DateColumnType extends AbstractColumnType {
     }
 
     @Override
-    public AbstractParser<LocalDate> customParser(CsvReadOptions options) {
+    public AbstractParser<LocalDate> customParser(ReadOptions options) {
         return new DateParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/dates/DateParser.java b/core/src/main/java/tech/tablesaw/columns/dates/DateParser.java
index fc25cfcb..ea4b65b1 100644
--- a/core/src/main/java/tech/tablesaw/columns/dates/DateParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/dates/DateParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.dates;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
@@ -61,7 +61,7 @@ public class DateParser extends AbstractParser<LocalDate> {
     private Locale locale = Locale.getDefault();
     private DateTimeFormatter formatter = DEFAULT_FORMATTER;
 
-    public DateParser(ColumnType type, CsvReadOptions readOptions) {
+    public DateParser(ColumnType type, ReadOptions readOptions) {
         super(type);
         DateTimeFormatter readCsvFormatter = readOptions.dateFormatter();
         if (readCsvFormatter != null) {
diff --git a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java
index c8f8bd4b..0bac2a5d 100644
--- a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.datetimes;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.DateTimeColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class DateTimeColumnType extends AbstractColumnType {
 
@@ -31,7 +31,7 @@ public class DateTimeColumnType extends AbstractColumnType {
     }
 
     @Override
-    public DateTimeParser customParser(CsvReadOptions options) {
+    public DateTimeParser customParser(ReadOptions options) {
         return new DateTimeParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeParser.java b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeParser.java
index a986fff3..a9048f86 100644
--- a/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/datetimes/DateTimeParser.java
@@ -5,7 +5,7 @@ import com.google.common.collect.Lists;
 
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
@@ -56,7 +56,7 @@ public class DateTimeParser extends AbstractParser<LocalDateTime> {
         super(columnType);
     }
 
-    public DateTimeParser(DateTimeColumnType dateTimeColumnType, CsvReadOptions readOptions) {
+    public DateTimeParser(DateTimeColumnType dateTimeColumnType, ReadOptions readOptions) {
         super(dateTimeColumnType);
         DateTimeFormatter readCsvFormatter = readOptions.dateFormatter();
         if (readCsvFormatter != null) {
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java
index b8c2b911..ac368d36 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.DoubleColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class DoubleColumnType extends AbstractColumnType {
 
@@ -11,6 +11,9 @@ public class DoubleColumnType extends AbstractColumnType {
 
     public static final DoubleParser DEFAULT_PARSER = new DoubleParser(ColumnType.DOUBLE);
 
+    private static DoubleColumnType INSTANCE =
+            new DoubleColumnType(BYTE_SIZE, "DOUBLE", "Double");
+
     public static DoubleColumnType instance() {
         if (INSTANCE == null) {
             INSTANCE = new DoubleColumnType(BYTE_SIZE, "DOUBLE", "Double");
@@ -18,9 +21,6 @@ public class DoubleColumnType extends AbstractColumnType {
         return INSTANCE;
     }
 
-    private static DoubleColumnType INSTANCE =
-            new DoubleColumnType(BYTE_SIZE, "DOUBLE", "Double");
-
     private DoubleColumnType(int byteSize, String name, String printerFriendlyName) {
         super(byteSize, name, printerFriendlyName);
     }
@@ -31,7 +31,7 @@ public class DoubleColumnType extends AbstractColumnType {
     }
 
     @Override
-    public DoubleParser customParser(CsvReadOptions options) {
+    public DoubleParser customParser(ReadOptions options) {
         return new DoubleParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleParser.java
index 18312883..6e004978 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/DoubleParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/DoubleParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class DoubleParser extends AbstractParser<Double> {
 
@@ -11,7 +11,7 @@ public class DoubleParser extends AbstractParser<Double> {
         super(columnType);
     }
 
-    public DoubleParser(DoubleColumnType doubleColumnType, CsvReadOptions readOptions) {
+    public DoubleParser(DoubleColumnType doubleColumnType, ReadOptions readOptions) {
         super(doubleColumnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/FloatColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/FloatColumnType.java
index 6a29f8aa..9b94053c 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/FloatColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/FloatColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.FloatColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class FloatColumnType extends AbstractColumnType {
 
@@ -30,7 +30,7 @@ public class FloatColumnType extends AbstractColumnType {
     }
 
     @Override
-    public FloatParser customParser(CsvReadOptions options) {
+    public FloatParser customParser(ReadOptions options) {
         return new FloatParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/FloatParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/FloatParser.java
index 3609bf92..3edae500 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/FloatParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/FloatParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class FloatParser extends AbstractParser<Float> {
 
@@ -11,7 +11,7 @@ public class FloatParser extends AbstractParser<Float> {
         super(columnType);
     }
 
-    public FloatParser(FloatColumnType columnType, CsvReadOptions readOptions) {
+    public FloatParser(FloatColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/IntColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/IntColumnType.java
index d703fbef..60452959 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/IntColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/IntColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.IntColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class IntColumnType extends AbstractColumnType {
 
@@ -30,7 +30,7 @@ public class IntColumnType extends AbstractColumnType {
     }
 
     @Override
-    public IntParser customParser(CsvReadOptions options) {
+    public IntParser customParser(ReadOptions options) {
         return new IntParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/IntParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/IntParser.java
index 2140f86a..77c47d9a 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/IntParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/IntParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class IntParser extends AbstractParser<Integer> {
 
@@ -11,7 +11,7 @@ public class IntParser extends AbstractParser<Integer> {
         super(columnType);
     }
 
-    public IntParser(IntColumnType columnType, CsvReadOptions readOptions) {
+    public IntParser(IntColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/LongColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/LongColumnType.java
index e833289a..f41c7d6b 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/LongColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/LongColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.LongColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class LongColumnType extends AbstractColumnType {
 
@@ -33,7 +33,7 @@ public class LongColumnType extends AbstractColumnType {
     }
 
     @Override
-    public LongParser customParser(CsvReadOptions options) {
+    public LongParser customParser(ReadOptions options) {
         return new LongParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/LongParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/LongParser.java
index eba352a8..14ecd6c0 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/LongParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/LongParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.numbers;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class LongParser extends AbstractParser<Long> {
 
@@ -11,7 +11,7 @@ public class LongParser extends AbstractParser<Long> {
         super(columnType);
     }
 
-    public LongParser(LongColumnType columnType, CsvReadOptions readOptions) {
+    public LongParser(LongColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/ShortColumnType.java b/core/src/main/java/tech/tablesaw/columns/numbers/ShortColumnType.java
index 53535233..0e287f65 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/ShortColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/ShortColumnType.java
@@ -2,7 +2,7 @@ package tech.tablesaw.columns.numbers;
 
 import tech.tablesaw.api.ShortColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class ShortColumnType extends AbstractColumnType {
 
@@ -29,7 +29,7 @@ public class ShortColumnType extends AbstractColumnType {
     }
 
     @Override
-    public ShortParser customParser(CsvReadOptions options) {
+    public ShortParser customParser(ReadOptions options) {
         return new ShortParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/numbers/ShortParser.java b/core/src/main/java/tech/tablesaw/columns/numbers/ShortParser.java
index 1a30e4d4..0e5572bd 100644
--- a/core/src/main/java/tech/tablesaw/columns/numbers/ShortParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/numbers/ShortParser.java
@@ -2,7 +2,7 @@ package tech.tablesaw.columns.numbers;
 
 import com.google.common.collect.Lists;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class ShortParser extends AbstractParser<Short> {
 
@@ -10,7 +10,7 @@ public class ShortParser extends AbstractParser<Short> {
         super(columnType);
     }
 
-    public ShortParser(ShortColumnType columnType, CsvReadOptions readOptions) {
+    public ShortParser(ShortColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java b/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java
index b67d8210..f9928cd4 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/StringColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.strings;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.StringColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class StringColumnType extends AbstractColumnType {
 
@@ -31,7 +31,7 @@ public class StringColumnType extends AbstractColumnType {
     }
 
     @Override
-    public StringParser customParser(CsvReadOptions options) {
+    public StringParser customParser(ReadOptions options) {
         return new StringParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/StringParser.java b/core/src/main/java/tech/tablesaw/columns/strings/StringParser.java
index 274a0f94..0abd62de 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/StringParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/StringParser.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.strings;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class StringParser extends AbstractParser<String> {
 
@@ -11,7 +11,7 @@ public class StringParser extends AbstractParser<String> {
         super(columnType);
     }
 
-    public StringParser(ColumnType columnType, CsvReadOptions readOptions) {
+    public StringParser(ColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         if (readOptions.missingValueIndicator() != null) {
             missingValueStrings = Lists.newArrayList(readOptions.missingValueIndicator());
diff --git a/core/src/main/java/tech/tablesaw/columns/strings/TextColumnType.java b/core/src/main/java/tech/tablesaw/columns/strings/TextColumnType.java
index cb7f89b1..3b490aad 100644
--- a/core/src/main/java/tech/tablesaw/columns/strings/TextColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/strings/TextColumnType.java
@@ -3,7 +3,7 @@ package tech.tablesaw.columns.strings;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.TextColumn;
 import tech.tablesaw.columns.AbstractColumnType;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 public class TextColumnType extends AbstractColumnType {
 
@@ -29,7 +29,7 @@ public class TextColumnType extends AbstractColumnType {
     }
 
     @Override
-    public StringParser customParser(CsvReadOptions options) {
+    public StringParser customParser(ReadOptions options) {
         return new StringParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java b/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java
index 48b47f14..ef3dd32d 100644
--- a/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java
+++ b/core/src/main/java/tech/tablesaw/columns/times/TimeColumnType.java
@@ -4,7 +4,7 @@ import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.TimeColumn;
 import tech.tablesaw.columns.AbstractColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalTime;
 
@@ -33,7 +33,7 @@ public class TimeColumnType extends AbstractColumnType {
     }
 
     @Override
-    public AbstractParser<LocalTime> customParser(CsvReadOptions options) {
+    public AbstractParser<LocalTime> customParser(ReadOptions options) {
         return new TimeParser(this, options);
     }
 
diff --git a/core/src/main/java/tech/tablesaw/columns/times/TimeParser.java b/core/src/main/java/tech/tablesaw/columns/times/TimeParser.java
index 4169d6c3..f8272102 100644
--- a/core/src/main/java/tech/tablesaw/columns/times/TimeParser.java
+++ b/core/src/main/java/tech/tablesaw/columns/times/TimeParser.java
@@ -4,7 +4,7 @@ import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.columns.AbstractParser;
-import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.ReadOptions;
 
 import java.time.LocalTime;
 import java.time.format.DateTimeFormatter;
@@ -63,7 +63,7 @@ public class TimeParser extends AbstractParser<LocalTime> {
         super(columnType);
     }
 
-    public TimeParser(ColumnType columnType, CsvReadOptions readOptions) {
+    public TimeParser(ColumnType columnType, ReadOptions readOptions) {
         super(columnType);
         DateTimeFormatter readCsvFormatter = readOptions.timeFormatter();
         if (readCsvFormatter != null) {
diff --git a/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java b/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
new file mode 100644
index 00000000..9aa376b2
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/ColumnTypeDetector.java
@@ -0,0 +1,210 @@
+package tech.tablesaw.io;
+
+import static tech.tablesaw.api.ColumnType.BOOLEAN;
+import static tech.tablesaw.api.ColumnType.DOUBLE;
+import static tech.tablesaw.api.ColumnType.FLOAT;
+import static tech.tablesaw.api.ColumnType.INTEGER;
+import static tech.tablesaw.api.ColumnType.LOCAL_DATE;
+import static tech.tablesaw.api.ColumnType.LOCAL_DATE_TIME;
+import static tech.tablesaw.api.ColumnType.LOCAL_TIME;
+import static tech.tablesaw.api.ColumnType.LONG;
+import static tech.tablesaw.api.ColumnType.SHORT;
+import static tech.tablesaw.api.ColumnType.STRING;
+import static tech.tablesaw.api.ColumnType.TEXT;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import com.google.common.collect.Lists;
+
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.columns.AbstractParser;
+import tech.tablesaw.columns.strings.StringColumnType;
+
+public class ColumnTypeDetector {
+
+    /**
+     * Consider using TextColumn instead of StringColumn for string data after this many rows
+     */
+    private static final int STRING_COLUMN_ROW_COUNT_CUTOFF = 50_000;
+
+    /**
+     * Use a TextColumn if at least this proportion of values are found to be unique in the type detection sample
+     *
+     * Note: This number is based on an assumption that as more records are considered, a smaller proportion of these
+     * new records will be found to be unique
+     *
+     * Sample calculation;
+     * 10 character string = 2 bytes * 10 + 38 extra bytes = 58; rounded up to 64 so it's a multiple of 8
+     *
+     * With dictionary encoding, we have 2*64 + 2*4 = 136 byte per unique value plus 4 bytes for each value
+     * For text columns we have 64 bytes per string
+     *
+     * So, if every value is unique, using dictionary encoding wastes about 70 bytes per value.
+     * If there are only two unique values, dictionary encoding saves about 62 bytes per value.
+     *
+     * Of course, it all depends on the lengths of the strings.
+     */
+    private static final double STRING_COLUMN_CUTOFF = 0.50;
+
+    /**
+     * Types to choose from. When more than one would work, we pick the first of the options. The order these appear in
+     * is critical. The broadest must go last, which is why String is at the end of the list. Any String read from
+     * the input will match string. If it were first on the list, you would get nothing but strings in your table.
+     *
+     * As another example, an integer type, should go before double. Otherwise double would match integers so
+     * the integer test would never be evaluated and all the ints would be read as doubles.
+     */
+    private List<ColumnType> typeArray =
+            Lists.newArrayList(
+                    LOCAL_DATE_TIME,
+                    LOCAL_TIME,
+                    LOCAL_DATE,
+                    BOOLEAN,
+                    SHORT,
+                    INTEGER,
+                    LONG,
+                    FLOAT,
+                    DOUBLE,
+                    STRING,
+                    TEXT);    
+
+    public ColumnTypeDetector() {}
+    
+    public ColumnTypeDetector(List<ColumnType> typeArrayOverrides) {
+	typeArray = typeArrayOverrides;
+    }
+    
+    /**
+     * Estimates and returns the type for each column in the input text
+     * <p>
+     * The type is determined by checking a sample of the data. Because only a sample of the data is
+     * checked, the types may be incorrect. If that is the case a Parse Exception will be thrown.
+     * <p>
+     * The method {@code printColumnTypes()} can be used to print a list of the detected columns that can be
+     * corrected and used to explicitly specify the correct column types.
+     */
+    public ColumnType[] detectColumnTypes(Iterator<String[]> rows, ReadOptions options) {
+        boolean useSampling = options.sample();
+
+        // to hold the results
+        List<ColumnType> columnTypes = new ArrayList<>();
+
+        // to hold the data read from the file
+        List<List<String>> columnData = new ArrayList<>();
+
+        int rowCount = 0; // make sure we don't go over maxRows
+
+        int nextRow = 0;
+        while (rows.hasNext()) {
+            String[] nextLine = rows.next();
+            // initialize the arrays to hold the strings. we don't know how many we need until we read the first row
+            if (rowCount == 0) {
+                for (int i = 0; i < nextLine.length; i++) {
+                    columnData.add(new ArrayList<>());
+                }
+            }
+            int columnNumber = 0;
+            if (rowCount == nextRow) {
+                for (String field : nextLine) {
+                    columnData.get(columnNumber).add(field);
+                    columnNumber++;
+                }
+                if (useSampling) {
+                    nextRow = nextRow(nextRow);
+                } else {
+                    nextRow = nextRowWithoutSampling(nextRow);
+                }
+            }
+            rowCount++;
+        }
+
+        // now detect
+        for (List<String> valuesList : columnData) {
+            ColumnType detectedType = detectType(valuesList, options);
+            if (detectedType.equals(StringColumnType.STRING) && rowCount > STRING_COLUMN_ROW_COUNT_CUTOFF) {
+                HashSet<String> unique = new HashSet<>(valuesList);
+                double uniquePct = unique.size() / (valuesList.size() * 1.0);
+                if (uniquePct > STRING_COLUMN_CUTOFF) {
+                    detectedType = TEXT;
+                }
+            }
+            columnTypes.add(detectedType);
+        }
+        return columnTypes.toArray(new ColumnType[0]);
+    }
+
+    private int nextRowWithoutSampling(int nextRow) {
+        return nextRow + 1;
+    }
+
+    private int nextRow(int nextRow) {
+        if (nextRow < 10_000) {
+            return nextRow + 1;
+        }
+        if (nextRow < 100_000) {
+            return nextRow + 1000;
+        }
+        if (nextRow < 1_000_000) {
+            return nextRow + 10_000;
+        }
+        if (nextRow < 10_000_000) {
+            return nextRow + 100_000;
+        }
+        if (nextRow < 100_000_000) {
+            return nextRow + 1_000_000;
+        }
+        return nextRow + 10_000_000;
+    }    
+
+    /**
+     * Returns a predicted ColumnType derived by analyzing the given list of undifferentiated strings read from a
+     * column in the file and applying the given Locale and options
+     */
+    private ColumnType detectType(List<String> valuesList, ReadOptions options) {
+
+        CopyOnWriteArrayList<AbstractParser<?>> parsers = new CopyOnWriteArrayList<>(getParserList(typeArray, options));
+
+        CopyOnWriteArrayList<ColumnType> typeCandidates = new CopyOnWriteArrayList<>(typeArray);
+
+        for (String s : valuesList) {
+            for (AbstractParser<?> parser : parsers) {
+                if (!parser.canParse(s)) {
+                    typeCandidates.remove(parser.columnType());
+                    parsers.remove(parser);
+                }
+            }
+        }
+        return selectType(typeCandidates);
+    }
+
+    /**
+     * Returns the selected candidate for a column of data, by picking the first value in the given list
+     *
+     * @param typeCandidates a possibly empty list of candidates. This list should be sorted in order of preference
+     */
+    private ColumnType selectType(List<ColumnType> typeCandidates) {
+        return typeCandidates.get(0);
+    }
+
+    /**
+     * Returns the list of parsers to use for type detection
+     *
+     * @param typeArray Array of column types. The order specifies the order the types are applied
+     * @param options CsvReadOptions to use to modify the default parsers for each type
+     * @return  A list of parsers in the order they should be used for type detection
+     */
+    private List<AbstractParser<?>> getParserList(List<ColumnType> typeArray, ReadOptions options) {
+        // Types to choose from. When more than one would work, we pick the first of the options
+
+        List<AbstractParser<?>> parsers = new ArrayList<>();
+        for (ColumnType type : typeArray) {
+            parsers.add(type.customParser(options));
+        }
+        return parsers;
+    }
+
+}
diff --git a/core/src/main/java/tech/tablesaw/io/DataFrameReader.java b/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
index 56bb003b..b2f2ed01 100644
--- a/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
+++ b/core/src/main/java/tech/tablesaw/io/DataFrameReader.java
@@ -14,19 +14,24 @@
 
 package tech.tablesaw.io;
 
-import tech.tablesaw.api.Table;
-import tech.tablesaw.io.csv.CsvReadOptions;
-import tech.tablesaw.io.csv.CsvReader;
-import tech.tablesaw.io.html.HtmlTableReader;
-import tech.tablesaw.io.jdbc.SqlResultSetReader;
-
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Reader;
 import java.io.StringReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.sql.ResultSet;
 import java.sql.SQLException;
+import java.util.Scanner;
+
+import tech.tablesaw.api.Table;
+import tech.tablesaw.io.csv.CsvReadOptions;
+import tech.tablesaw.io.csv.CsvReader;
+import tech.tablesaw.io.html.HtmlTableReader;
+import tech.tablesaw.io.jdbc.SqlResultSetReader;
+import tech.tablesaw.io.json.JsonReader;
 
 public class DataFrameReader {
 
@@ -62,11 +67,22 @@ public class DataFrameReader {
         return new CsvReader().read(options);
     }
 
+    public Table json(String url) throws MalformedURLException, IOException {
+        try (Scanner scanner = new Scanner(new URL(url).openStream(), StandardCharsets.UTF_8.toString())) {
+            scanner.useDelimiter("\\A");
+            return json(new StringReader(scanner.hasNext() ? scanner.next() : ""), url);
+	}
+    }
+
+    public Table json(Reader contents, String tableName) throws IOException {
+	return new JsonReader().read(ReadOptions.builder(contents, tableName).build());
+    }
+
     public Table db(ResultSet resultSet, String tableName) throws SQLException {
         return SqlResultSetReader.read(resultSet, tableName);
     }
 
     public Table html(String url) throws IOException {
-        return csv(new HtmlTableReader().tableToCsv(url), url);
+        return new HtmlTableReader().read(url);
     }
 }
diff --git a/core/src/main/java/tech/tablesaw/io/ReadOptions.java b/core/src/main/java/tech/tablesaw/io/ReadOptions.java
new file mode 100644
index 00000000..b3abb94f
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/ReadOptions.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package tech.tablesaw.io;
+
+import com.google.common.base.Strings;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.Reader;
+import java.time.format.DateTimeFormatter;
+import java.util.Locale;
+
+public class ReadOptions {
+
+    // we always have one of these (file, reader, or inputStream)
+    protected final File file;
+    protected final Reader reader;
+    protected final InputStream inputStream;
+
+    protected final String tableName;
+    protected final boolean sample;
+    protected final String dateFormat;
+    protected final String dateTimeFormat;
+    protected final String timeFormat;
+    protected final Locale locale;
+    protected final String missingValueIndicator;
+
+    protected ReadOptions(ReadOptions.Builder builder) {
+
+        int sourceCount = 0;
+        if (builder.file != null) sourceCount++;
+        if (builder.reader != null) sourceCount++;
+        if (builder.inputStream != null) sourceCount++;
+
+        if (sourceCount == 0) {
+            throw new IllegalArgumentException("CsvReadOptions Builder configured with no data source");
+        } else if (sourceCount > 1) {
+            throw new IllegalArgumentException("CsvReadOptions Builder configured with more than one data source");
+        }
+
+        file = builder.file;
+        reader = builder.reader;
+        inputStream = builder.inputStream;
+        tableName = builder.tableName;
+        sample = builder.sample;
+        dateFormat = builder.dateFormat;
+        timeFormat = builder.timeFormat;
+        dateTimeFormat = builder.dateTimeFormat;
+        missingValueIndicator = builder.missingValueIndicator;
+
+        if (builder.locale == null) {
+            locale = Locale.getDefault();
+        } else {
+            locale = builder.locale;
+        }
+    }
+
+    public static Builder builder(File file) {
+        return new Builder(file).tableName(file.getName());
+    }
+
+    public static Builder builder(String fileName) {
+        return new Builder(new File(fileName));
+    }
+
+    /**
+     * This method may cause tablesaw to buffer the entire InputStream.
+     * <p>
+     * If you have a large amount of data, you can do one of the following:
+     * 1. Use the method taking a File instead of a stream, or
+     * 2. Provide the array of column types as an option. If you provide the columnType array,
+     * we skip type detection and can avoid reading the entire file
+     */
+    public static Builder builder(InputStream stream, String tableName) {
+        return new Builder(stream).tableName(tableName);
+    }
+
+    /**
+     * This method may cause tablesaw to buffer the entire InputStream.
+     *
+     * <p>
+     * If you have a large amount of data, you can do one of the following:
+     * 1. Use the method taking a File instead of a reader, or
+     * 2. Provide the array of column types as an option. If you provide the columnType array,
+     * we skip type detection and can avoid reading the entire file
+     */
+    public static Builder builder(Reader reader, String tableName) {
+        Builder builder = new Builder(reader);
+        return builder.tableName(tableName);
+    }
+
+    public File file() {
+        return file;
+    }
+
+    public Reader reader() {
+        return reader;
+    }
+
+    public InputStream inputStream() {
+        return inputStream;
+    }
+
+    public String tableName() {
+        return tableName;
+    }
+
+    public boolean sample() {
+        return sample;
+    }
+
+    public String missingValueIndicator() {
+        return missingValueIndicator;
+    }
+
+    public Locale locale() {
+        return locale;
+    }
+
+    public DateTimeFormatter dateTimeFormatter() {
+        if (Strings.isNullOrEmpty(dateTimeFormat)) {
+            return null;
+        }
+        return DateTimeFormatter.ofPattern(dateTimeFormat, locale);
+    }
+
+    public DateTimeFormatter timeFormatter() {
+        if (Strings.isNullOrEmpty(timeFormat)) {
+            return null;
+        }
+        return DateTimeFormatter.ofPattern(timeFormat, locale);
+    }
+
+    public DateTimeFormatter dateFormatter() {
+        if (Strings.isNullOrEmpty(dateFormat)) {
+            return null;
+        }
+        return DateTimeFormatter.ofPattern(dateFormat, locale);
+    }
+
+    public static class Builder {
+
+        protected InputStream inputStream;
+        protected File file;
+        protected Reader reader;
+        protected String tableName = "";
+        protected boolean sample = true;
+        protected String dateFormat;
+        protected String timeFormat;
+        protected String dateTimeFormat;
+        protected Locale locale;
+        protected String missingValueIndicator;
+
+        public Builder(File file) {
+            this.file = file;
+            this.tableName = file.getName();
+        }
+
+        /**
+         * This method may cause tablesaw to buffer the entire InputStream.
+         * <p>
+         * If you have a large amount of data, you can do one of the following:
+         * 1. Use the method taking a File instead of a reader, or
+         * 2. Provide the array of column types as an option. If you provide the columnType array,
+         * we skip type detection and can avoid reading the entire file
+         */
+        public Builder(Reader reader) {
+            this.reader = reader;
+        }
+
+        /**
+         * This method may cause tablesaw to buffer the entire InputStream.
+         * <p>
+         * If you have a large amount of data, you can do one of the following:
+         * 1. Use the method taking a File instead of a stream, or
+         * 2. Provide the array of column types as an option. If you provide the columnType array,
+         * we skip type detection and can avoid reading the entire file
+         */
+        public Builder(InputStream stream) {
+            this.inputStream = stream;
+        }
+
+        public Builder tableName(String tableName) {
+            this.tableName = tableName;
+            return this;
+        }
+
+        public Builder dateFormat(String dateFormat) {
+            this.dateFormat = dateFormat;
+            return this;
+        }
+
+        public Builder timeFormat(String timeFormat) {
+            this.timeFormat = timeFormat;
+            return this;
+        }
+
+        public Builder dateTimeFormat(String dateTimeFormat) {
+            this.dateTimeFormat = dateTimeFormat;
+            return this;
+        }
+
+        public Builder missingValueIndicator(String missingValueIndicator) {
+            this.missingValueIndicator = missingValueIndicator;
+            return this;
+        }
+
+        public Builder sample(boolean sample) {
+            this.sample = sample;
+            return this;
+        }
+
+        public Builder locale(Locale locale) {
+            this.locale = locale;
+            return this;
+        }
+
+        public ReadOptions build() {
+            return new ReadOptions(this);
+        }
+    }
+
+}
diff --git a/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java b/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java
new file mode 100644
index 00000000..61f8faba
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/TableBuildingUtils.java
@@ -0,0 +1,52 @@
+package tech.tablesaw.io;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Iterator;
+import java.util.List;
+
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.api.Table;
+
+public class TableBuildingUtils {
+
+    public static Reader createReader(ReadOptions options, byte[] cachedBytes) throws IOException {
+	if (cachedBytes != null) {
+	    return new InputStreamReader(new ByteArrayInputStream(cachedBytes));
+	}
+        if (options.inputStream() != null) {
+            return new InputStreamReader(options.inputStream());
+        }
+        if (options.reader() != null) {
+            return options.reader();
+        }
+        return new FileReader(options.file());
+    }
+
+    public static Table build(List<String> columnNames, List<String[]> dataRows, ReadOptions options) {
+        Table table = Table.create(options.tableName());
+
+        if (dataRows.size() == 0) {
+            return table;
+        }
+
+        ColumnTypeDetector detector = new ColumnTypeDetector();
+        Iterator<String[]> iterator = dataRows.iterator();
+        ColumnType[] types = detector.detectColumnTypes(iterator, options);
+        for (int i = 0; i < columnNames.size(); i++) {
+            table.addColumns(types[i].create(columnNames.get(i)));
+        }
+
+        for (int i = 0; i < dataRows.size(); i++) {
+            for (int j = 0; j < table.columnCount(); j++) {
+                table.column(j).appendCell(dataRows.get(i)[j]);        	
+            }
+        }
+
+        return table;
+    }
+
+}
diff --git a/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java b/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java
index df13c6af..74c5a64a 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/AddCellToColumnException.java
@@ -15,6 +15,7 @@
 package tech.tablesaw.io.csv;
 
 import java.io.PrintStream;
+import java.util.List;
 
 /**
  * This Exception wraps another Exception thrown while adding a cell to a column.
@@ -42,7 +43,7 @@ public class AddCellToColumnException extends RuntimeException {
     /**
      * The column names stored as an array
      */
-    private final String[] columnNames;
+    private final List<String> columnNames;
 
     /**
      * The original line that caused the Exception
@@ -58,8 +59,8 @@ public class AddCellToColumnException extends RuntimeException {
      * @param columnNames The column names stored as an array
      * @param line        The original line that caused the Exception
      */
-    public AddCellToColumnException(Exception e, int columnIndex, long rowNumber, String[] columnNames, String[] line) {
-        super("Error while adding cell from row " + rowNumber + " and column " + columnNames[columnIndex] + "" +
+    public AddCellToColumnException(Exception e, int columnIndex, long rowNumber, List<String> columnNames, String[] line) {
+        super("Error while adding cell from row " + rowNumber + " and column " + columnNames.get(columnIndex) + "" +
                 "(position:" + columnIndex + "): " + e.getMessage(), e);
         this.columnIndex = columnIndex;
         this.rowNumber = rowNumber;
@@ -84,7 +85,7 @@ public class AddCellToColumnException extends RuntimeException {
     /**
      * Returns the column names array
      */
-    public String[] getColumnNames() {
+    public List<String> getColumnNames() {
         return columnNames;
     }
 
@@ -92,7 +93,7 @@ public class AddCellToColumnException extends RuntimeException {
      * Returns the name of the column that caused the Exception
      */
     public String getColumnName() {
-        return columnNames[columnIndex];
+        return columnNames.get(columnIndex);
     }
 
     /**
@@ -101,11 +102,11 @@ public class AddCellToColumnException extends RuntimeException {
      * @param out The PrintStream to output to
      */
     public void dumpRow(PrintStream out) {
-        for (int i = 0; i < columnNames.length; i++) {
+        for (int i = 0; i < columnNames.size(); i++) {
             out.print("Column ");
             out.print(i);
             out.print(" ");
-            out.print(columnNames[i]);
+            out.print(columnNames.get(columnIndex));
             out.print(" : ");
             try {
                 out.println(line[i]);
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java b/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
index feda60a8..5f1cf616 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReadOptions.java
@@ -16,6 +16,7 @@ package tech.tablesaw.io.csv;
 
 import com.google.common.base.Strings;
 import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.io.ReadOptions;
 
 import java.io.File;
 import java.io.InputStream;
@@ -23,59 +24,22 @@ import java.io.Reader;
 import java.time.format.DateTimeFormatter;
 import java.util.Locale;
 
-public class CsvReadOptions {
+public class CsvReadOptions extends ReadOptions {
 
-    // we always have one of these (file, reader, or inputStream)
-    private final File file;
-    private final Reader reader;
-    private final InputStream inputStream;
-
-    private final String tableName;
     private final ColumnType[] columnTypes;
     private final boolean header;
     private final Character separator;
     private final String lineEnding;
-    private final boolean sample;
-    private final String dateFormat;
-    private final String dateTimeFormat;
-    private final String timeFormat;
-    private final Locale locale;
-    private final String missingValueIndicator;
     private final Integer maxNumberOfColumns;
 
     private CsvReadOptions(CsvReadOptions.Builder builder) {
+	super(builder);
 
-        int sourceCount = 0;
-        if (builder.file != null) sourceCount++;
-        if (builder.reader != null) sourceCount++;
-        if (builder.inputStream != null) sourceCount++;
-
-        if (sourceCount == 0) {
-            throw new IllegalArgumentException("CsvReadOptions Builder configured with no data source");
-        } else if (sourceCount > 1) {
-            throw new IllegalArgumentException("CsvReadOptions Builder configured with more than one data source");
-        }
-
-        file = builder.file;
-        reader = builder.reader;
-        inputStream = builder.inputStream;
-        tableName = builder.tableName;
         columnTypes = builder.columnTypes;
         header = builder.header;
         separator = builder.separator;
-        sample = builder.sample;
-        dateFormat = builder.dateFormat;
-        timeFormat = builder.timeFormat;
-        dateTimeFormat = builder.dateTimeFormat;
         lineEnding = builder.lineEnding;
-        missingValueIndicator = builder.missingValueIndicator;
         maxNumberOfColumns = builder.maxNumberOfColumns;
-
-        if (builder.locale == null) {
-            locale = Locale.getDefault();
-        } else {
-            locale = builder.locale;
-        }
     }
 
     public static Builder builder(File file) {
@@ -181,27 +145,16 @@ public class CsvReadOptions {
         return maxNumberOfColumns;
     }
 
-    public static class Builder {
+    public static class Builder extends ReadOptions.Builder {
 
-        private InputStream inputStream;
-        private File file;
-        private Reader reader;
-        private String tableName = "";
         private boolean header = true;
         private Character separator = ',';
         private String lineEnding;
-        private boolean sample = true;
         private ColumnType[] columnTypes;
-        private String dateFormat;
-        private String timeFormat;
-        private String dateTimeFormat;
-        private Locale locale;
-        private String missingValueIndicator;
         private Integer maxNumberOfColumns = 10_000;
 
         public Builder(File file) {
-            this.file = file;
-            this.tableName = file.getName();
+            super(file);
         }
 
         /**
@@ -213,7 +166,7 @@ public class CsvReadOptions {
          * we skip type detection and can avoid reading the entire file
          */
         public Builder(Reader reader) {
-            this.reader = reader;
+            super(reader);
         }
 
         /**
@@ -225,7 +178,7 @@ public class CsvReadOptions {
          * we skip type detection and can avoid reading the entire file
          */
         public Builder(InputStream stream) {
-            this.inputStream = stream;
+            super(stream);
         }
 
         public Builder tableName(String tableName) {
diff --git a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
index c33b5f4d..ac7ec8f2 100644
--- a/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
+++ b/core/src/main/java/tech/tablesaw/io/csv/CsvReader.java
@@ -16,7 +16,6 @@ package tech.tablesaw.io.csv;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
-import com.google.common.io.ByteStreams;
 import com.google.common.io.CharStreams;
 import com.univocity.parsers.csv.CsvFormat;
 import com.univocity.parsers.csv.CsvParser;
@@ -25,76 +24,27 @@ import tech.tablesaw.api.ColumnType;
 import tech.tablesaw.api.Table;
 import tech.tablesaw.columns.AbstractParser;
 import tech.tablesaw.columns.Column;
-import tech.tablesaw.columns.strings.StringColumnType;
+import tech.tablesaw.io.ColumnTypeDetector;
+import tech.tablesaw.io.TableBuildingUtils;
 
 import javax.annotation.concurrent.Immutable;
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
+
+import org.apache.commons.lang3.tuple.Pair;
+
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.Reader;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
-import java.util.concurrent.CopyOnWriteArrayList;
 
 import static tech.tablesaw.api.ColumnType.*;
 
 @Immutable
 public class CsvReader {
 
-    /**
-     * Consider using TextColumn instead of StringColumn for string data after this many rows
-     */
-    private static final int STRING_COLUMN_ROW_COUNT_CUTOFF = 50_000;
-
-    /**
-     * Use a TextColumn if at least this proportion of values are found to be unique in the type detection sample
-     *
-     * Note: This number is based on an assumption that as more records are considered, a smaller proportion of these
-     * new records will be found to be unique
-     *
-     * Sample calculation;
-     * 10 character string = 2 bytes * 10 + 38 extra bytes = 58; rounded up to 64 so it's a multiple of 8
-     *
-     * With dictionary encoding, we have 2*64 + 2*4 = 136 byte per unique value plus 4 bytes for each value
-     * For text columns we have 64 bytes per string
-     *
-     * So, if every value is unique, using dictionary encoding wastes about 70 bytes per value.
-     * If there are only two unique values, dictionary encoding saves about 62 bytes per value.
-     *
-     * Of course, it all depends on the lengths of the strings.
-     */
-    private static final double STRING_COLUMN_CUTOFF = 0.50;
-
-    /**
-     * Types to choose from. When more than one would work, we pick the first of the options. The order these appear in
-     * is critical. The broadest must go last, which is why String is at the end of the list. Any String read from
-     * a CSV will match string. If it were first on the list, you would get nothing but strings in your table.
-     *
-     * As another example, an integer type, should go before double. Otherwise double would match integers so
-     * the integer test would never be evaluated and all the ints would be read as doubles.
-     */
-    private List<ColumnType> typeArray =
-            Lists.newArrayList(
-                    LOCAL_DATE_TIME,
-                    LOCAL_TIME,
-                    LOCAL_DATE,
-                    BOOLEAN,
-                    SHORT,
-                    INTEGER,
-                    LONG,
-                    FLOAT,
-                    DOUBLE,
-                    STRING,
-                    TEXT);
+    private List<ColumnType> typeArrayOverrides = null;
 
     /**
      * Constructs a CsvReader
@@ -107,43 +57,50 @@ public class CsvReader {
      * These are the only types that the CsvReader can detect and parse
      */
     public CsvReader(List<ColumnType> typeDetectionList) {
-        this.typeArray = typeDetectionList;
+        this.typeArrayOverrides = typeDetectionList;
     }
 
-    public Table read(CsvReadOptions options) throws IOException {
-
+    /**
+     * Determines column types if not provided by the user
+     * Reads all input into memory unless File was provided
+     */
+    private Pair<Reader, ColumnType[]> getReaderAndColumnTypes(CsvReadOptions options) throws IOException {
         ColumnType[] types = options.columnTypes();
-        byte[] bytes = null;
+        byte[] bytesCache = null;
 
         if (types == null) {
-            if (options.reader() != null) {
-                bytes = CharStreams.toString(options.reader()).getBytes();
+            Reader reader = TableBuildingUtils.createReader(options, bytesCache);
+            if (options.file() == null) {
+        	bytesCache = CharStreams.toString(reader).getBytes();
+        	// create a new reader since we just exhausted the existing one
+        	reader = TableBuildingUtils.createReader(options, bytesCache);
             }
-            if (options.inputStream() != null) {
-                bytes = ByteStreams.toByteArray(options.inputStream());
-            }
-            types = getColumnTypes(options, bytes);
+            types = detectColumnTypes(reader, options);
         }
 
-        Reader reader = getReader(options, bytes);
+        return Pair.of(TableBuildingUtils.createReader(options, bytesCache), types);
+    }
+
+    public Table read(CsvReadOptions options) throws IOException {
+        return read(options, false);
+    }
+
+    private Table read(CsvReadOptions options, boolean headerOnly) throws IOException {
+	Pair<Reader, ColumnType[]> pair = getReaderAndColumnTypes(options);
+	Reader reader = pair.getLeft();
+	ColumnType[] types = pair.getRight();
+
         CsvParser parser = csvParser(options);
 
         try {
             parser.beginParsing(reader);
             Table table = Table.create(options.tableName());
 
-            String[] headerNames = getHeaderNames(options, types, parser);
-            if (headerNames == null) {
-                return table;
-            }
-            List<String> headerRow = Lists.newArrayList(headerNames);
-
-            String[] columnNames = selectColumnNames(headerRow, types);
+            List<String> headerRow = Lists.newArrayList(getHeaderNames(options, types, parser));
 
-            cleanNames(headerRow);
             for (int x = 0; x < types.length; x++) {
                 if (types[x] != SKIP) {
-                    String columnName = headerRow.get(x);
+                    String columnName = cleanName(headerRow.get(x));
                     if (Strings.isNullOrEmpty(columnName)) {
                         columnName = "Column " + table.columnCount();
                     }
@@ -151,13 +108,17 @@ public class CsvReader {
                     table.addColumns(newColumn);
                 }
             }
-            int[] columnIndexes = new int[columnNames.length];
-            for (int i = 0; i < columnIndexes.length; i++) {
-                // get the index in the original table, which includes skipped fields
-                columnIndexes[i] = headerRow.indexOf(columnNames[i]);
+
+            if (!headerOnly) {
+                String[] columnNames = selectColumnNames(headerRow, types);
+                int[] columnIndexes = new int[columnNames.length];
+                for (int i = 0; i < columnIndexes.length; i++) {
+                    // get the index in the original table, which includes skipped fields
+                    columnIndexes[i] = headerRow.indexOf(columnNames[i]);
+                }
+                addRows(options, types, parser, table, columnIndexes);
             }
 
-            addRows(options, types, parser, table, columnNames, columnIndexes);
             return table;
         } finally {
             if (options.reader() == null) {
@@ -170,86 +131,62 @@ public class CsvReader {
     }
 
     private String[] getHeaderNames(CsvReadOptions options, ColumnType[] types, CsvParser parser) {
-        String[] headerNames;
         if (options.header()) {
-            headerNames = parser.parseNext();
+            String[] headerNames = parser.parseNext();
             // work around issue where Univocity returns null if a column has no header.
             for (int i = 0; i < headerNames.length; i++) {
                 if (headerNames[i] == null) {
                     headerNames[i] = "C" + i;
                 }
             }
+            return headerNames;
         } else {
-            headerNames = makeColumnNames(types);
-        }
-        return headerNames;
-    }
-
-    private Reader getReader(CsvReadOptions options, byte[] bytes)
-            throws FileNotFoundException {
-
-        if (bytes != null) {
-            return new InputStreamReader(new ByteArrayInputStream(bytes));
-        }
-        if (options.inputStream() != null) {
-            return new InputStreamReader(options.inputStream());
-        }
-        if (options.reader() != null) {
-            return options.reader();
-        }
-        return new InputStreamReader(new FileInputStream(options.file()));
-    }
-
-    /**
-     * Returns column types for this table as an array, the types are either provided in read options, or calculated by
-     * scanning the data
-     */
-    private ColumnType[] getColumnTypes(CsvReadOptions options, byte[] bytes) throws IOException {
-        ColumnType[] types;
-        try(InputStream detectTypesStream = options.file() != null
-                ? new FileInputStream(options.file())
-                : new ByteArrayInputStream(bytes)) {
-            types = detectColumnTypes(detectTypesStream, options);
+            // Placeholder column names for when the file read has no header
+            String[] headerNames = new String[types.length];
+            for (int i = 0; i < types.length; i++) {
+        	headerNames[i] = "C" + i;
+            }
+            return headerNames;
         }
-        return types;
     }
 
-    private void addRows(CsvReadOptions options, ColumnType[] types, CsvParser reader, Table table, String[] columnNames, int[] columnIndexes) {
-        long rowNumber = options.header() ? 1L : 0L;
+    private void addRows(CsvReadOptions options, ColumnType[] types, CsvParser reader, Table table, int[] columnIndexes) {
         String[] nextLine;
 
         Map<String, AbstractParser<?>> parserMap = getParserMap(options, table);
 
         // Add the rows
-        while ((nextLine = reader.parseNext()) != null) {
-
+        for (long rowNumber = options.header() ? 1L : 0L; (nextLine = reader.parseNext()) != null; rowNumber++) {
+            // validation
             if (nextLine.length < types.length) {
                 if (nextLine.length == 1 && Strings.isNullOrEmpty(nextLine[0])) {
                     System.err.println("Warning: Invalid CSV file. Row "
                             + rowNumber
                             + " is empty. Continuing.");
+                    continue;
                 } else {
-                    Exception e = new IndexOutOfBoundsException("Row number " + rowNumber + " is too short.");
-                    throw new AddCellToColumnException(e, 0, rowNumber, columnNames, nextLine);
+                    Exception e = new IndexOutOfBoundsException("Row number " + rowNumber + " contains " + nextLine.length + " columns. "
+                    	+ types.length + " expected.");
+                    throw new AddCellToColumnException(e, 0, rowNumber, table.columnNames(), nextLine);
                 }
             } else if (nextLine.length > types.length) {
-                throw new IllegalArgumentException("Row number " + rowNumber + " is too long.");
-            } else {
-                // for each column that we're including (not skipping)
-                int cellIndex = 0;
-                for (int columnIndex : columnIndexes) {
-                    Column<?> column = table.column(cellIndex);
-                    AbstractParser<?> parser = parserMap.get(column.name());
-                    try {
-                        String value = nextLine[columnIndex];
-                        column.appendCell(value, parser);
-                    } catch (Exception e) {
-                        throw new AddCellToColumnException(e, columnIndex, rowNumber, columnNames, nextLine);
-                    }
-                    cellIndex++;
+                throw new IllegalArgumentException("Row number " + rowNumber + " contains " + nextLine.length + " columns. "
+                	+ types.length + " expected.");
+            }
+
+            // append each column that we're including (not skipping)
+            int cellIndex = 0;
+            for (int columnIndex : columnIndexes) {
+                Column<?> column = table.column(cellIndex);
+                AbstractParser<?> parser = parserMap.get(column.name());
+                try {
+                    String value = nextLine[columnIndex];
+                    column.appendCell(value, parser);
+                } catch (Exception e) {
+                    throw new AddCellToColumnException(e, columnIndex, rowNumber, table.columnNames(), nextLine);
                 }
+                cellIndex++;
             }
-            rowNumber++;
         }
     }
 
@@ -262,94 +199,8 @@ public class CsvReader {
         return parserMap;
     }
 
-    private void cleanNames(List<String> headerRow) {
-        for (int i = 0; i < headerRow.size(); i++) {
-            headerRow.set(i, headerRow.get(i).trim());
-        }
-    }
-
-    /**
-     * Returns a Table constructed from a CSV File with the given file name
-     * <p>
-     * The @code{fileName} is used as the initial table name for the new table
-     *
-     * @param types           An array of the types of columns in the file, in the order they appear
-     * @param header          Is the first row in the file a header?
-     * @param options         Sets the format for and instructions for parsing
-     * @param file            The fully specified file name. It is used to provide a default name for the table
-     * @return                A Relation containing the data in the csv file.
-     * @throws IOException    if file cannot be read
-     */
-    public Table headerOnly(ColumnType[] types, boolean header, CsvReadOptions options, File file)
-            throws IOException {
-
-        FileInputStream fis = new FileInputStream(file);
-
-        Reader reader = new InputStreamReader(fis);
-
-        Table table;
-
-        CsvParser csvParser = csvParser(options);
-        try (BufferedReader streamReader = new BufferedReader(reader)) {
-            csvParser.beginParsing(streamReader);
-
-            String[] columnNames;
-            List<String> headerRow;
-
-            String[] headerNames;
-            if (header) {
-                headerNames = csvParser.parseNext();
-                // work around issue where Univocity returns null if a column has no header.
-                for (int i = 0; i < headerNames.length; i++) {
-                    if (headerNames[i] == null) {
-                        headerNames[i] = "C" + i;
-                    }
-                }
-            } else {
-                headerNames = makeColumnNames(types);
-            }
-
-            headerRow = Lists.newArrayList(headerNames);
-            columnNames = selectColumnNames(headerRow, types);
-
-            table = Table.create(file.getName());
-            for (int x = 0; x < types.length; x++) {
-                if (types[x] != SKIP) {
-                    Column<?> newColumn = types[x].create(headerRow.get(x).trim());
-                    table.addColumns(newColumn);
-                }
-            }
-            int[] columnIndexes = new int[columnNames.length];
-            for (int i = 0; i < columnIndexes.length; i++) {
-                // get the index in the original table, which includes skipped fields
-                columnIndexes[i] = headerRow.indexOf(columnNames[i]);
-            }
-        } finally {
-            // the stream is already closed
-            csvParser.stopParsing();
-        }
-        return table;
-    }
-
-    /**
-     * Returns the structure of the table given by {@code csvFileName} as detected by analysis of a sample of the data
-     *
-     * @throws IOException if file cannot be read
-     */
-    private Table detectedColumnTypes(String csvFileName, boolean header, char delimiter, Locale locale) throws IOException {
-        File file = new File(csvFileName);
-        try (InputStream stream = new FileInputStream(file)) {
-
-            CsvReadOptions options = CsvReadOptions.builder(stream, "")
-                    .separator(delimiter)
-                    .header(header)
-                    .locale(locale)
-                    .sample(true)
-                    .build();
-            ColumnType[] types = detectColumnTypes(stream, options);
-            Table t = headerOnly(types, header, options, file);
-            return t.structure();
-        }
+    private String cleanName(String name) {
+	return name.trim();
     }
 
     /**
@@ -371,9 +222,9 @@ public class CsvReader {
      *
      * @throws IOException if file cannot be read
      */
-    public String printColumnTypes(String csvFileName, boolean header, char delimiter, Locale locale) throws IOException {
+    public String printColumnTypes(CsvReadOptions options) throws IOException {
 
-        Table structure = detectedColumnTypes(csvFileName, header, delimiter, locale);
+        Table structure = read(options, true).structure();
 
         StringBuilder buf = new StringBuilder();
         buf.append("ColumnType[] columnTypes = {");
@@ -429,17 +280,6 @@ public class CsvReader {
         return header.toArray(result);
     }
 
-    /**
-     * Provides placeholder column names for when the file read has no header
-     */
-    private String[] makeColumnNames(ColumnType[] types) {
-        String[] header = new String[types.length];
-        for (int i = 0; i < types.length; i++) {
-            header[i] = "C" + i;
-        }
-        return header;
-    }
-
     /**
      * Estimates and returns the type for each column in the delimited text file {@code file}
      * <p>
@@ -451,141 +291,43 @@ public class CsvReader {
      * corrected and
      * used to explicitly specify the correct column types.
      */
-    public ColumnType[] detectColumnTypes(InputStream stream, CsvReadOptions options) {
+    protected ColumnType[] detectColumnTypes(Reader reader, CsvReadOptions options) {
 
         boolean header = options.header();
-        boolean useSampling = options.sample();
-
         int linesToSkip = header ? 1 : 0;
 
-        // to hold the results
-        List<ColumnType> columnTypes = new ArrayList<>();
-
-        // to hold the data read from the file
-        List<List<String>> columnData = new ArrayList<>();
-
-        int rowCount = 0; // make sure we don't go over maxRows
-
         CsvParser csvParser = csvParser(options);
 
         try {
-            csvParser.beginParsing(new InputStreamReader(stream));
+            csvParser.beginParsing(reader);
 
             for (int i = 0; i < linesToSkip; i++) {
                 csvParser.parseNext();
             }
 
-            String[] nextLine;
-            int nextRow = 0;
-            while ((nextLine = csvParser.parseNext()) != null) {
-                // initialize the arrays to hold the strings. we don't know how many we need until we read the first row
-                if (rowCount == 0) {
-                    for (int i = 0; i < nextLine.length; i++) {
-                        columnData.add(new ArrayList<>());
-                    }
-                }
-                int columnNumber = 0;
-                if (rowCount == nextRow) {
-                    for (String field : nextLine) {
-                        columnData.get(columnNumber).add(field);
-                        columnNumber++;
-                    }
-                    if (useSampling) {
-                        nextRow = nextRow(nextRow);
-                    } else {
-                        nextRow = nextRowWithoutSampling(nextRow);
-                    }
-                }
-                rowCount++;
-            }
+            ColumnTypeDetector detector = typeArrayOverrides == null
+        	    ? new ColumnTypeDetector() : new ColumnTypeDetector(typeArrayOverrides);
+            return detector.detectColumnTypes(new Iterator<String[]>() {
+
+        	String[] nextRow = csvParser.parseNext();
+
+		@Override
+		public boolean hasNext() {
+		    return nextRow != null;
+		}
+
+		@Override
+		public String[] next() {
+		    String[] tmp = nextRow;
+		    nextRow = csvParser.parseNext();
+		    return tmp;
+		}
+        	
+            }, options);
         } finally {
             csvParser.stopParsing();
             // we don't close the reader since we didn't create it
         }
-
-        // now detect
-        for (List<String> valuesList : columnData) {
-            ColumnType detectedType = detectType(valuesList, options);
-            if (detectedType.equals(StringColumnType.STRING) && rowCount > STRING_COLUMN_ROW_COUNT_CUTOFF) {
-                HashSet<String> unique = new HashSet<>(valuesList);
-                double uniquePct = unique.size() / (valuesList.size() * 1.0);
-                if (uniquePct > STRING_COLUMN_CUTOFF) {
-                    detectedType = TEXT;
-                }
-            }
-            columnTypes.add(detectedType);
-        }
-        return columnTypes.toArray(new ColumnType[0]);
-    }
-
-    private int nextRowWithoutSampling(int nextRow) {
-        return nextRow + 1;
-    }
-
-    private int nextRow(int nextRow) {
-        if (nextRow < 10_000) {
-            return nextRow + 1;
-        }
-        if (nextRow < 100_000) {
-            return nextRow + 1000;
-        }
-        if (nextRow < 1_000_000) {
-            return nextRow + 10_000;
-        }
-        if (nextRow < 10_000_000) {
-            return nextRow + 100_000;
-        }
-        if (nextRow < 100_000_000) {
-            return nextRow + 1_000_000;
-        }
-        return nextRow + 10_000_000;
-    }
-
-    /**
-     * Returns a predicted ColumnType derived by analyzing the given list of undifferentiated strings read from a
-     * column in the file and applying the given Locale and options
-     */
-    private ColumnType detectType(List<String> valuesList, CsvReadOptions options) {
-
-        CopyOnWriteArrayList<AbstractParser<?>> parsers = new CopyOnWriteArrayList<>(getParserList(typeArray, options));
-
-        CopyOnWriteArrayList<ColumnType> typeCandidates = new CopyOnWriteArrayList<>(typeArray);
-
-        for (String s : valuesList) {
-            for (AbstractParser<?> parser : parsers) {
-                if (!parser.canParse(s)) {
-                    typeCandidates.remove(parser.columnType());
-                    parsers.remove(parser);
-                }
-            }
-        }
-        return selectType(typeCandidates);
-    }
-
-    /**
-     * Returns the selected candidate for a column of data, by picking the first value in the given list
-     *
-     * @param typeCandidates a possibly empty list of candidates. This list should be sorted in order of preference
-     */
-    private ColumnType selectType(List<ColumnType> typeCandidates) {
-        return typeCandidates.get(0);
-    }
-
-    /**
-     * Returns the list of parsers to use for type detection
-     *
-     * @param typeArray Array of column types. The order specifies the order the types are applied
-     * @param options CsvReadOptions to use to modify the default parsers for each type
-     * @return  A list of parsers in the order they should be used for type detection
-     */
-    private List<AbstractParser<?>> getParserList(List<ColumnType> typeArray, CsvReadOptions options) {
-        // Types to choose from. When more than one would work, we pick the first of the options
-
-        List<AbstractParser<?>> parsers = new ArrayList<>();
-        for (ColumnType type : typeArray) {
-            parsers.add(type.customParser(options));
-        }
-        return parsers;
     }
 
     private CsvParser csvParser(CsvReadOptions options) {
@@ -608,10 +350,4 @@ public class CsvReader {
         return format;
     }
 
-    /**
-     * Returns the list of types that specifies the order in which types are tested in the detection algorithm.
-     */
-    public List<ColumnType> getTypeArray() {
-        return typeArray;
-    }
 }
diff --git a/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java b/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java
index 9f1e9962..63b592a6 100644
--- a/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java
+++ b/core/src/main/java/tech/tablesaw/io/html/HtmlTableReader.java
@@ -1,19 +1,23 @@
 package tech.tablesaw.io.html;
 
-import com.univocity.parsers.csv.CsvWriter;
-import com.univocity.parsers.csv.CsvWriterSettings;
+import tech.tablesaw.api.Table;
+import tech.tablesaw.io.ReadOptions;
+import tech.tablesaw.io.TableBuildingUtils;
+
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 import org.jsoup.select.Elements;
 
 import java.io.IOException;
-import java.io.StringWriter;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.stream.Stream;
 
 public class HtmlTableReader {
 
-    public String tableToCsv(String url) throws IOException {
+    public Table read(String url) throws IOException { // TODO: take ReaderOptions. Add a test using a File
         Document doc = Jsoup.connect(url).get();
         Elements tables = doc.select("table");
         if (tables.size() != 1) {
@@ -22,18 +26,31 @@ public class HtmlTableReader {
                             + " The URL you passed has " + tables.size()
                             + ". You may file a feature request with the URL if you'd like your pagae to be supported");
         }
-        Element table = tables.get(0);
-        CsvWriterSettings settings = new CsvWriterSettings();
-        StringWriter stringWriter = new StringWriter();
-        CsvWriter csvWriter = new CsvWriter(stringWriter, settings);
+        Element htmlTable = tables.get(0);
 
-        for (Element row : table.select("tr")) {
+        List<String[]> rows = new ArrayList<>();
+        for (Element row : htmlTable.select("tr")) {
             Elements headerCells = row.getElementsByTag("th");
             Elements cells = row.getElementsByTag("td");
             String[] nextLine = Stream.concat(headerCells.stream(), cells.stream())
                     .map(Element::text).toArray(String[]::new);
-            csvWriter.writeRow(nextLine);
+            rows.add(nextLine);
+        }
+
+        Table table = Table.create(url);
+
+        if (rows.size() == 0) {
+            return table;
         }
-        return stringWriter.toString();
+
+        ReadOptions options = ReadOptions.builder(new StringReader(""), url).build(); // TODO: this should be passed in
+        String[] headerRow = rows.get(0);
+        List<String> columnNames = new ArrayList<>();
+        for (int i = 0; i < headerRow.length; i++) {
+            columnNames.add(headerRow[i]); // TODO: cleansing and fallback name
+        }
+
+        return TableBuildingUtils.build(columnNames, rows, options);
     }
+
 }
diff --git a/core/src/main/java/tech/tablesaw/io/json/JsonReader.java b/core/src/main/java/tech/tablesaw/io/json/JsonReader.java
new file mode 100644
index 00000000..dc7f594d
--- /dev/null
+++ b/core/src/main/java/tech/tablesaw/io/json/JsonReader.java
@@ -0,0 +1,98 @@
+package tech.tablesaw.io.json;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.wnameless.json.flattener.JsonFlattener;
+
+import tech.tablesaw.api.Table;
+import tech.tablesaw.io.ReadOptions;
+import tech.tablesaw.io.TableBuildingUtils;
+
+public class JsonReader {
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    public Table read(ReadOptions options) throws IOException {
+        JsonNode jsonObj = mapper.readTree(TableBuildingUtils.createReader(options, null));
+        if (!jsonObj.isArray()) {
+            throw new IllegalStateException(
+                    "Only reading a json array or arrays or objects is currently supported");
+        }
+        if (jsonObj.size() == 0) {
+            return Table.create(options.tableName());
+        }
+
+        JsonNode firstNode = jsonObj.get(0);
+        if (firstNode.isArray()) {
+            return convertArrayOfArrays(jsonObj, options);
+        }
+        return convertArrayOfObjects(jsonObj, options);
+    }
+
+    private Table convertArrayOfArrays(JsonNode jsonObj, ReadOptions options) {
+        JsonNode firstNode = jsonObj.get(0);
+	boolean firstRowAllStrings = true;
+        List<String> columnNames = new ArrayList<>();
+        for (JsonNode n : firstNode) {
+    	if (!n.isTextual()) {
+    	    firstRowAllStrings = false;
+    	}
+        }
+        boolean hasHeader = firstRowAllStrings;
+        for (int i = 0; i < firstNode.size(); i++) {
+    	columnNames.add(hasHeader ? firstNode.get(i).textValue() : "Column " + i);
+        }
+        List<String[]> dataRows = new ArrayList<>();
+        for (int i = hasHeader ? 1 : 0; i < jsonObj.size(); i++) {
+    	JsonNode arr = jsonObj.get(i);
+            String[] row = new String[arr.size()];
+            for (int j = 0; j < arr.size(); j++) {
+                row[j] = arr.get(j).asText();
+            }
+            dataRows.add(row);
+        }
+        return TableBuildingUtils.build(columnNames, dataRows, options);
+    }
+
+    private Table convertArrayOfObjects(JsonNode jsonObj, ReadOptions options) throws IOException {
+        // flatten each object inside the array
+        StringBuilder result = new StringBuilder("[");
+        for (int i = 0; i < jsonObj.size(); i++) {
+            JsonNode rowObj = jsonObj.get(i);
+            String flattenedRow = JsonFlattener.flatten(mapper.writeValueAsString(rowObj));
+            if (i != 0) {
+                result.append(",");
+            }
+            result.append(flattenedRow);
+        }
+        String flattenedJsonString = result.append("]").toString();
+        JsonNode flattenedJsonObj = mapper.readTree(flattenedJsonString);
+
+        Set<String> colNames = new HashSet<>();
+        for (JsonNode row : flattenedJsonObj) {
+            Iterator<String> fieldNames = row.fieldNames();
+            while (fieldNames.hasNext()) {
+        	colNames.add(fieldNames.next());
+            }
+        }
+
+        List<String> columnNames = new ArrayList<>(colNames);
+        List<String[]> dataRows = new ArrayList<>();
+        for (JsonNode node : flattenedJsonObj) {
+            String[] arr = new String[columnNames.size()];
+            for (int i = 0; i < columnNames.size(); i++) {
+        	arr[i] = node.get(columnNames.get(i)).asText();
+            }
+            dataRows.add(arr);
+        }
+
+        return TableBuildingUtils.build(columnNames, dataRows, options);
+    }
+}
diff --git a/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java b/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
index eff58deb..422ca410 100644
--- a/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
+++ b/core/src/main/java/tech/tablesaw/table/TableSliceGroup.java
@@ -67,11 +67,11 @@ public class TableSliceGroup implements Iterable<TableSlice> {
         this.splitColumnNames = groupColumnNames;
     }
 
-    String[] getSplitColumnNames() {
+    protected String[] getSplitColumnNames() {
         return splitColumnNames;
     }
 
-    int getByteSize(List<Column<?>> columns) {
+    protected int getByteSize(List<Column<?>> columns) {
         int byteSize = 0;
         for (Column<?> c : columns) {
             byteSize += c.byteSize();
@@ -79,7 +79,7 @@ public class TableSliceGroup implements Iterable<TableSlice> {
         return byteSize;
     }
 
-    void addSlice(TableSlice slice) {
+    protected void addSlice(TableSlice slice) {
         subTables.add(slice);
     }
 
@@ -211,7 +211,7 @@ public class TableSliceGroup implements Iterable<TableSlice> {
         return tableList;
     }
 
-    void setSourceTable(Table sourceTable) {
+    protected void setSourceTable(Table sourceTable) {
         this.sourceTable = sourceTable;
     }
 }
diff --git a/docs/contributing.md b/docs/contributing.md
index dd734061..099a14dd 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -47,14 +47,10 @@ To summarize:
 ### "Help Wanted" labels
 Among the open bugs and feature requests, we've tagged some as "Help Wanted". This indicates that a fix should be relatively straightforward to address. We would, of course, LOVE help on some of the more challenging issues. Help Wanted simply means that this is a good place for a developer with limited knowlege of the library and/or limited experience with Java to jump in. 
 
-### Javascript visualisation libraries
+### Javascript visualization libraries
 Implement an interface for a Javascript plotting library (big job, we know), but the best Javascript libraries are awesome.
 Email larry@tablesaw.tech if you have any questions, or use the comments for the issue.
 
-## Working on the code in Eclipse
-
-* Install e(fx)clipse. Choose the update site that came with your installation of Eclipse. E.g. "Oxygen - http://download.eclipse.org/releases/oxygen". Then install "e(fx)clipse" under "General Purpose Tools"
-
 ## Become a Maintainer/Committer
 
 We are actively seeking new maintainers for the project. The best way to get involved is to familiarize yourself with the project through use, and make a few minor changes/fixes. After that, introduce yourself by emailing info@tablesaw.tech and we’ll get in touch.
diff --git a/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java b/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
index f95d40f7..4c6e7ff8 100644
--- a/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
+++ b/jsplot/src/main/java/tech/tablesaw/plotly/traces/TraceBuilder.java
@@ -72,7 +72,7 @@ public abstract class TraceBuilder {
         return this;
     }
 
-    static String[] columnToStringArray(Column<?> column) {
+    protected static String[] columnToStringArray(Column<?> column) {
         String[] x = new String[column.size()];
         for (int i = 0; i < column.size(); i++) {
             x[i] = column.getString(i);


Test patch:
diff --git a/core/src/test/java/tech/tablesaw/api/TimeColumnTest.java b/core/src/test/java/tech/tablesaw/api/TimeColumnTest.java
index 23261966..3791d32e 100644
--- a/core/src/test/java/tech/tablesaw/api/TimeColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/api/TimeColumnTest.java
@@ -25,7 +25,6 @@ import java.nio.ByteBuffer;
 import java.time.LocalTime;
 import java.time.temporal.ChronoUnit;
 import java.util.List;
-import java.util.Set;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -147,18 +146,6 @@ public class TimeColumnTest {
         assertNotEquals(LocalTime.NOON, column1.get(3));
     }
 
-    @Test
-    public void testAsSet() {
-        column1.appendCell("10:15:30");
-        column1.appendCell("11:30:00");
-        column1.appendCell("14:00:00");
-        column1.appendCell("18:15:30");
-        column1.appendInternal(of(12,0, 0));
-        column1.appendInternal(of(12,0, 0));
-        Set<LocalTime> set = column1.asSet();
-        assertEquals(column1.size(), set.size() + 1);
-    }
-
     @Test
     public void testAppendCell2() {
         column1.appendCell("12:18:03 AM");
diff --git a/core/src/test/java/tech/tablesaw/columns/ColumnTest.java b/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
index 9cc33bc7..59b54ee3 100644
--- a/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
+++ b/core/src/test/java/tech/tablesaw/columns/ColumnTest.java
@@ -200,7 +200,7 @@ public class ColumnTest {
         assertContentEquals(filtered, 0.0, 1.0);
     }
 
-    static String getSeason(LocalDate date) {
+    private static String getSeason(LocalDate date) {
         String season = "";
         int month = date.getMonthValue();
         int day = date.getDayOfMonth();
diff --git a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
index 5b9ffcd0..3cd93ef8 100644
--- a/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
+++ b/core/src/test/java/tech/tablesaw/io/csv/CsvReaderTest.java
@@ -25,12 +25,12 @@ import tech.tablesaw.api.LongColumn;
 import tech.tablesaw.api.ShortColumn;
 import tech.tablesaw.api.Table;
 
-import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileReader;
 import java.io.InputStream;
 import java.io.Reader;
+import java.io.StringReader;
 import java.nio.file.Paths;
 import java.time.ZoneOffset;
 import java.util.Collections;
@@ -124,15 +124,15 @@ public class CsvReaderTest {
 
     @Test
     public void testDataTypeDetection() throws Exception {
-        InputStream stream = new FileInputStream(new File("../data/bus_stop_test.csv"));
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        Reader reader = new FileReader("../data/bus_stop_test.csv");
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(true)
                 .separator(',')
                 .sample(false)
                 .locale(Locale.getDefault())
                 .build();
 
-        ColumnType[] columnTypes = new CsvReader().detectColumnTypes(stream, options);
+        ColumnType[] columnTypes = new CsvReader().detectColumnTypes(reader, options);
         assertArrayEquals(bus_types, columnTypes);
     }
 
@@ -147,27 +147,27 @@ public class CsvReaderTest {
     @Test
     public void testLocalDateDetectionEnglish() {
 
-        final InputStream stream = new ByteArrayInputStream((
+        final Reader reader = new StringReader(
                 "Date" + LINE_END
             + "\"Nov 1, 2017\"" + LINE_END
             + "\"Oct 1, 2017\"" + LINE_END
             + "\"Sep 1, 2017\"" + LINE_END
             + "\"Aug 1, 2017\"" + LINE_END
             + "\"Jul 1, 2017\"" + LINE_END
-            + "\"Jun 1, 2017\"" + LINE_END).getBytes());
+            + "\"Jun 1, 2017\"" + LINE_END);
 
         final boolean header = true;
         final char delimiter = ',';
         final boolean useSampling = true;
 
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(header)
                 .separator(delimiter)
                 .sample(useSampling)
                 .locale(Locale.ENGLISH)
                 .build();
 
-        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(stream, options));
+        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(reader, options));
 
         assertThat(actual, is(equalTo(Collections.singletonList(LOCAL_DATE))));
     }
@@ -175,27 +175,27 @@ public class CsvReaderTest {
     @Test
     public void testLocalDateTimeDetectionEnglish() {
 
-        final InputStream stream = new ByteArrayInputStream((
+        final Reader reader = new StringReader(
               "Date" + LINE_END
             + "09-Nov-2014 13:03" + LINE_END
             + "09-Oct-2014 13:03" + LINE_END
             + "09-Sep-2014 13:03" + LINE_END
             + "09-Aug-2014 13:03" + LINE_END
             + "09-Jul-2014 13:03" + LINE_END
-            + "09-Jun-2014 13:03" + LINE_END).getBytes());
+            + "09-Jun-2014 13:03" + LINE_END);
 
         final boolean header = true;
         final char delimiter = ',';
         final boolean useSampling = true;
 
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(header)
                 .separator(delimiter)
                 .sample(useSampling)
                 .locale(Locale.ENGLISH)
                 .build();
 
-        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(stream, options));
+        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(reader, options));
 
         assertThat(actual, is(equalTo(Collections.singletonList(LOCAL_DATE_TIME))));
 
@@ -204,27 +204,27 @@ public class CsvReaderTest {
     @Test
     public void testLocalDateDetectionFrench() {
 
-        final InputStream stream = new ByteArrayInputStream((
+        final Reader reader = new StringReader(
                 "Date" + LINE_END
             + "\"nov. 1, 2017\"" + LINE_END
             + "\"oct. 1, 2017\"" + LINE_END
             + "\"sept. 1, 2017\"" + LINE_END
             + "\"août 1, 2017\"" + LINE_END
             + "\"juil. 1, 2017\"" + LINE_END
-            + "\"juin 1, 2017\""+ LINE_END).getBytes());
+            + "\"juin 1, 2017\""+ LINE_END);
 
         final boolean header = true;
         final char delimiter = ',';
         final boolean useSampling = true;
 
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(header)
                 .separator(delimiter)
                 .sample(useSampling)
                 .locale(Locale.FRENCH)
                 .build();
 
-        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(stream, options));
+        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(reader, options));
 
         assertThat(actual, is(equalTo(Collections.singletonList(LOCAL_DATE))));
     }
@@ -232,27 +232,27 @@ public class CsvReaderTest {
     @Test
     public void testLocalDateTimeDetectionFrench() {
 
-        final InputStream stream = new ByteArrayInputStream((
+        final Reader reader = new StringReader(
               "Date" + LINE_END
             + "09-nov.-2014 13:03" + LINE_END
             + "09-oct.-2014 13:03" + LINE_END
             + "09-sept.-2014 13:03" + LINE_END
             + "09-août-2014 13:03" + LINE_END
             + "09-juil.-2014 13:03" + LINE_END
-            + "09-juin-2014 13:03" + LINE_END).getBytes());
+            + "09-juin-2014 13:03" + LINE_END);
 
         final boolean header = true;
         final char delimiter = ',';
         final boolean useSampling = true;
 
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(header)
                 .separator(delimiter)
                 .sample(useSampling)
                 .locale(Locale.FRENCH)
                 .build();
 
-        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(stream, options));
+        final List<ColumnType> actual = asList(new CsvReader().detectColumnTypes(reader, options));
 
         assertThat(actual, is(equalTo(Collections.singletonList(LOCAL_DATE_TIME))));
     }
@@ -313,20 +313,25 @@ public class CsvReaderTest {
                         "STRING,     // 2     who         " + LINE_END +
                         "}" + LINE_END;
         assertEquals(output, new CsvReader()
-                .printColumnTypes("../data/bush.csv", true, ',', Locale.getDefault()));
+                .printColumnTypes(CsvReadOptions.builder("../data/bush.csv")
+                	.header(true)
+                	.separator(',')
+                	.locale(Locale.getDefault())
+                	.sample(true)
+                	.build()));
     }
 
     @Test
     public void testDataTypeDetection2() throws Exception {
-        InputStream stream = new FileInputStream(new File("../data/bush.csv"));
-        CsvReadOptions options = CsvReadOptions.builder(stream, "")
+        Reader reader = new FileReader("../data/bush.csv");
+        CsvReadOptions options = CsvReadOptions.builder(reader, "")
                 .header(true)
                 .separator(',')
                 .sample(false)
                 .locale(Locale.getDefault())
                 .build();
 
-        ColumnType[] columnTypes = new CsvReader().detectColumnTypes(stream, options);
+        ColumnType[] columnTypes = new CsvReader().detectColumnTypes(reader, options);
         assertEquals(LOCAL_DATE, columnTypes[0]);
         assertEquals(SHORT, columnTypes[1]);
         assertEquals(STRING, columnTypes[2]);
@@ -473,11 +478,4 @@ public class CsvReaderTest {
         assertEquals("1 rows X 10001 cols", table1.shape());
     }
 
-    @Test
-    public void getTypeArray() {
-        List<ColumnType> types = new CsvReader().getTypeArray();
-        for (ColumnType type : types) {
-            assertNotNull(type);
-        }
-    }
 }
diff --git a/core/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java b/core/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
new file mode 100644
index 00000000..55b2c50b
--- /dev/null
+++ b/core/src/test/java/tech/tablesaw/io/json/JsonReaderTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package tech.tablesaw.io.json;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import org.junit.Test;
+
+import tech.tablesaw.api.ColumnType;
+import tech.tablesaw.api.Table;
+
+public class JsonReaderTest {
+
+    @Test
+    public void arrayOfArraysWithHeader() throws IOException {
+	String json = "[[\"Date\",\"Value\"],[1453438800000,-2.1448117025014],[1454043600000,-2.9763153817574],[1454648400000,-2.9545283436391]]";
+	Table table = Table.read().json(new StringReader(json), "jsonTable");
+        assertEquals(2, table.columnCount());
+        assertEquals(3, table.rowCount());
+        assertEquals("Date", table.column(0).name());
+        assertEquals("Value", table.column(1).name());
+        assertEquals(ColumnType.LONG, table.columnTypes()[0]);
+    }
+
+    @Test
+    public void arrayOfArraysNoHeader() throws IOException {
+	String json = "[[1453438800000,-2.1448117025014],[1454043600000,-2.9763153817574],[1454648400000,-2.9545283436391]]";
+	Table table = Table.read().json(new StringReader(json), "jsonTable");
+        assertEquals(2, table.columnCount());
+        assertEquals(3, table.rowCount());
+        assertEquals(ColumnType.LONG, table.columnTypes()[0]);
+    }
+
+    @Test
+    public void arrayOfNestedObjects() throws IOException {
+	String json = "[{\"a\":1453438800000,\"b\":{\"c\":-2.1448117025014}},{\"a\":1454043600000,\"b\":{\"c\":-2.9763153817574}},{\"a\":1454648400000,\"b\":{\"c\":-2.9545283436391}}]";
+	Table table = Table.read().json(new StringReader(json), "jsonTable");
+        assertEquals(2, table.columnCount());
+        assertEquals(3, table.rowCount());
+        assertEquals("a", table.column(0).name());
+        assertEquals("b.c", table.column(1).name());
+        assertEquals(ColumnType.LONG, table.columnTypes()[0]);
+    }
+
+}
diff --git a/jsplot/src/test/java/tech/tablesaw/examples/MultiPlotExample.java b/jsplot/src/test/java/tech/tablesaw/examples/MultiPlotExample.java
index 57c6aa21..305aed51 100644
--- a/jsplot/src/test/java/tech/tablesaw/examples/MultiPlotExample.java
+++ b/jsplot/src/test/java/tech/tablesaw/examples/MultiPlotExample.java
@@ -17,6 +17,30 @@ import java.util.List;
 
 public class MultiPlotExample {
 
+    private static final String pageTop = "<html>" + System.lineSeparator() +
+        "<head>" + System.lineSeparator() +
+        "    <title>Multi-plot test</title>" + System.lineSeparator() +
+        "    <script src=\"https://cdn.plot.ly/plotly-latest.min.js\"></script>" + System.lineSeparator() +
+        "</head>" + System.lineSeparator() +
+        "<body>" + System.lineSeparator() +
+        "<div id='plot1'>" + System.lineSeparator() +
+        "<div id='plot2'>" + System.lineSeparator();
+
+    private static final String pageBottom =
+        "</body>" + System.lineSeparator() + "</html>";
+
+    private static String makePage(Figure figure1, Figure figure2, String divName1, String divName2) {
+        return new StringBuilder()
+                .append(pageTop)
+                .append(System.lineSeparator())
+                .append(figure1.asJavascript(divName1))
+                .append(System.lineSeparator())
+                .append(figure2.asJavascript(divName2))
+                .append(System.lineSeparator())
+                .append(pageBottom)
+                .toString();
+    }
+
     public static void main(String[] args) throws Exception {
 
         /*
@@ -86,29 +110,4 @@ public class MultiPlotExample {
         new Browser().browse(outputFile);
     }
 
-    private static String makePage(Figure figure1, Figure figure2, String divName1, String divName2) {
-
-        return new StringBuilder()
-                .append(pageTop)
-                .append(System.lineSeparator())
-                .append(figure1.asJavascript(divName1))
-                .append(System.lineSeparator())
-                .append(figure2.asJavascript(divName2))
-                .append(System.lineSeparator())
-                .append(pageBottom)
-                .toString();
-    }
-
-    private static final String pageTop = "<html>" + System.lineSeparator() +
-            "<head>" + System.lineSeparator() +
-            "    <title>Multi-plot test</title>" + System.lineSeparator() +
-            "    <script src=\"https://cdn.plot.ly/plotly-latest.min.js\"></script>" + System.lineSeparator() +
-            "</head>" + System.lineSeparator() +
-            "<body>" + System.lineSeparator() +
-            "<div id='plot1'>" + System.lineSeparator() +
-            "<div id='plot2'>" + System.lineSeparator();
-
-    private static final String pageBottom =
-            "</body>" + System.lineSeparator() +
-                    "</html>";
 }