You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by js...@apache.org on 2020/04/28 08:19:40 UTC

[sis] branch feat/geojson updated (6c65e3d -> d80be71)

This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a change to branch feat/geojson
in repository https://gitbox.apache.org/repos/asf/sis.git.


 discard 6c65e3d  GeoJson : reuse FeatureComparator in tests
 discard 979f433  GeoJson : add GeoJson DataStore
     add a731452  Fix a NullPointerException caused by the absence of CRS in the envelope inferred from a GridExtent.
     add d85273e  Pom : disable trimStackTrace by default in surefire plugin
     add 8bb0edb  Make a better effort to take the `gridToCRS` transform in account when determining a CRS for a GridExtent.
     add 04d422a  Feature : add FeatureComparator utility test class
     add da1e14d  Provides more guidance to user when selecting a CRS by warning when the CRS domain of validity does not intersect the area of interest.
     add 1918205  First draft of a `ChoiceBox` for choosing a CRS in a list of more recently used CRS.
     add 939ec62  Take in account the geographic area of data shown when building the list of CRS.
     add b0cf8d5  Consolidation of CRSChooser in situations where an error occurs during the construction of a CRS. Before this commit, the CRSChooser behavior in such case was confusion (e.g. filtering not working anymore).
     add 1a3bcee  Fix a NullPointerException.
     add 3734fbe  Give public access to `ImageLayout` constructor for allowing the use of different preferred tile size.
     add c277c9c  Deprecate a method which is not used in practice and cause compatibility problem with JDK 14.
     add 11f8694  Keep a relationship between the CRS of coordinates shown in the status bar and the CRS of the displayed raster.
     add e3a2b62  doc(Referencing): minor note on envelope utility method.
     add bd836fa  Fix a NullPointerException when building a `GridCoverage2D` with domain set to `GridGeometry.UNDEFINED`.
     add 65deba1  Change `GeneralEnvelope.setTimeRange(…)` contract regarding null values in a way more convenient for creating "is before" and "is after" filters. Add some documentation.
     add 0707557  Make `CoordinateFormat` more robust to change of CRS.
     add dbec36d  Apply CRS choice on the coordinate values displayed.
     add 3ff7c26  Resolve numerous problems with the display of geographic/projected coordinates under change of CRS.
     add 9005422  Show CRS name in a tooltip and axis abbreviations.
     add beece97  Make check for JNDI context a little bit more robust.
     add b729668  Fix a problem at initialization of CRS choices (was initialized to the wrong CRS).
     add 6404696  Fix other CRS selection problems (wrong CRS checked in menu items, "Other…" not working). This commit fixes the last problems we have seen so far.
     add b0e8264  Refactor StatusBar API with some method renaming, property definitions and javadoc completionS. No change in functionalities.
     add 71da3a6  When formatting projected coordinates, give also the axis direction. Example: "-100 m E 300 m N". Maybe a future version should replace "E" by "W" when the value is negative, but current version is enough for resolving the ambiguity problem.
     add 38ab0ef  Make CoordinateFormat.parse(…) consistent with CoordinateFormat.format(…) regarding direction and accuracy information.
     add e474c3d  Replace a NullPointerException (at `build()` invocation time) by a more explicit exception thrown earlier.
     add f9f9e2b  Add missing serial version.
     add b44873a  Initial implementation of Cassini-Soldner projection written from EPSG formulas. This commit provides a direct, non-optimized implementation with almost verbatim copy of formulas published in:
     new d80be71  GeoJson : add GeoJson DataStore

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (6c65e3d)
            \
             N -- N -- N   refs/heads/feat/geojson (d80be71)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/java/org/apache/sis/gui/DataViewer.java   |    1 +
 .../java/org/apache/sis/gui/coverage/Controls.java |   34 +-
 .../apache/sis/gui/coverage/CoverageControls.java  |   54 +-
 .../apache/sis/gui/coverage/CoverageExplorer.java  |   52 +-
 .../org/apache/sis/gui/coverage/GridControls.java  |   12 +-
 .../java/org/apache/sis/gui/coverage/GridView.java |   38 +-
 .../org/apache/sis/gui/coverage/ImageRequest.java  |    8 +-
 .../org/apache/sis/gui/dataset/ResourceTree.java   |    2 +-
 .../java/org/apache/sis/gui/map/StatusBar.java     |  757 +++++++++++---
 .../java/org/apache/sis/gui/metadata/Section.java  |    2 +-
 .../apache/sis/gui/referencing/AuthorityCodes.java |  284 ++++--
 .../org/apache/sis/gui/referencing/CRSChooser.java |  267 ++++-
 .../org/apache/sis/gui/referencing/CodeFilter.java |    8 +-
 .../org/apache/sis/gui/referencing/MenuSync.java   |  247 +++++
 .../sis/gui/referencing/ObjectStringConverter.java |  110 +++
 .../gui/referencing/RecentReferenceSystems.java    |  851 ++++++++++++++++
 .../java/org/apache/sis/gui/referencing/Utils.java |   93 ++
 .../org/apache/sis/gui/referencing/WKTPane.java    |   32 +-
 .../apache/sis/internal/gui/ExceptionReporter.java |   10 +
 .../org/apache/sis/internal/gui/GUIUtilities.java  |  255 +++++
 .../org/apache/sis/internal/gui/RecentChoices.java |   87 ++
 .../org/apache/sis/internal/gui/Resources.java     |   42 +-
 .../apache/sis/internal/gui/Resources.properties   |   34 +-
 .../sis/internal/gui/Resources_fr.properties       |   34 +-
 .../java/org/apache/sis/internal/gui/Styles.java   |   93 +-
 .../apache/sis/gui/referencing/CRSChooserApp.java  |   92 ++
 .../apache/sis/internal/gui/GUIUtilitiesTest.java  |   21 +-
 .../sis/test/suite/ApplicationTestSuite.java       |   14 +-
 .../sis/coverage/grid/GridCoverageBuilder.java     |    2 +-
 .../apache/sis/coverage/grid/GridDerivation.java   |    1 +
 .../org/apache/sis/coverage/grid/GridExtent.java   |   12 +-
 .../apache/sis/coverage/grid/GridExtentCRS.java    |  196 ++++
 .../org/apache/sis/coverage/grid/GridGeometry.java |    7 +-
 .../java/org/apache/sis/image/ComputedImage.java   |    4 +-
 .../sis/internal/coverage/j2d/ImageLayout.java     |    2 +-
 .../sis/coverage/grid/GridCoverageBuilderTest.java |   14 +
 .../apache/sis/coverage/grid/GridExtentTest.java   |   39 +-
 .../sis/internal/metadata/sql/Initializer.java     |    3 +-
 .../apache/sis/metadata/iso/extent/Extents.java    |   17 +-
 .../org/apache/sis/geometry/CoordinateFormat.java  | 1030 +++++++++++++++-----
 .../org/apache/sis/geometry/GeneralEnvelope.java   |   20 +-
 .../{Polyconic.java => CassiniSoldner.java}        |   68 +-
 .../referencing/provider/PolarStereographicA.java  |    2 +-
 .../apache/sis/referencing/GeodeticCalculator.java |    3 +-
 .../operation/projection/CassiniSoldner.java       |  176 ++++
 ...g.opengis.referencing.operation.OperationMethod |    1 +
 .../apache/sis/geometry/CoordinateFormatTest.java  |   85 +-
 .../referencing/provider/ProvidersTest.java        |    1 +
 .../java/org/apache/sis/io/CompoundFormat.java     |    7 +-
 .../org/apache/sis/measure/QuantityFormat.java     |  146 +++
 .../main/java/org/apache/sis/measure/Scalar.java   |    4 +-
 .../java/org/apache/sis/measure/UnitFormat.java    |    4 +-
 .../java/org/apache/sis/util/CharSequences.java    |    2 +-
 .../main/java/org/apache/sis/util/Characters.java  |   12 +-
 .../sis/util/collection/FrequencySortedSet.java    |    3 +
 .../org/apache/sis/util/resources/Vocabulary.java  |   20 +
 .../sis/util/resources/Vocabulary.properties       |    4 +
 .../sis/util/resources/Vocabulary_fr.properties    |    4 +
 .../java/org/apache/sis/measure/ScalarTest.java    |   10 +-
 .../java/org/apache/sis/util/CharactersTest.java   |   27 +-
 ide-project/NetBeans/nbproject/build-impl.xml      |   22 +-
 ide-project/NetBeans/nbproject/genfiles.properties |    4 +-
 ide-project/NetBeans/nbproject/project.properties  |    1 +
 ide-project/NetBeans/nbproject/project.xml         |    2 +
 pom.xml                                            |    2 +-
 65 files changed, 4701 insertions(+), 790 deletions(-)
 create mode 100644 application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/MenuSync.java
 create mode 100644 application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/ObjectStringConverter.java
 create mode 100644 application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/RecentReferenceSystems.java
 create mode 100644 application/sis-javafx/src/main/java/org/apache/sis/gui/referencing/Utils.java
 create mode 100644 application/sis-javafx/src/main/java/org/apache/sis/internal/gui/GUIUtilities.java
 create mode 100644 application/sis-javafx/src/test/java/org/apache/sis/gui/referencing/CRSChooserApp.java
 copy core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/TableInfoTest.java => application/sis-javafx/src/test/java/org/apache/sis/internal/gui/GUIUtilitiesTest.java (64%)
 copy profiles/sis-japan-profile/src/test/java/org/apache/sis/test/suite/JapanProfileTestSuite.java => application/sis-javafx/src/test/java/org/apache/sis/test/suite/ApplicationTestSuite.java (77%)
 create mode 100644 core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtentCRS.java
 copy core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/provider/{Polyconic.java => CassiniSoldner.java} (74%)
 create mode 100644 core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/CassiniSoldner.java
 create mode 100644 core/sis-utility/src/main/java/org/apache/sis/measure/QuantityFormat.java


[sis] 01/01: GeoJson : add GeoJson DataStore

Posted by js...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

jsorel pushed a commit to branch feat/geojson
in repository https://gitbox.apache.org/repos/asf/sis.git

commit d80be716b96503eb2d35fefcbb9cfcd454da9678
Author: jsorel <jo...@geomatys.com>
AuthorDate: Tue Oct 29 17:07:57 2019 +0100

    GeoJson : add GeoJson DataStore
---
 storage/pom.xml                                    |   1 +
 storage/{ => sis-geojson}/pom.xml                  |  98 +--
 .../sis/internal/geojson/FeatureTypeUtils.java     | 597 +++++++++++++++
 .../apache/sis/internal/geojson/GeoJSONParser.java | 691 +++++++++++++++++
 .../apache/sis/internal/geojson/GeoJSONUtils.java  | 580 +++++++++++++++
 .../sis/internal/geojson/LiteJsonLocation.java     | 112 +++
 .../sis/internal/geojson/binding/GeoJSONCRS.java   |  93 +++
 .../internal/geojson/binding/GeoJSONFeature.java   |  68 ++
 .../geojson/binding/GeoJSONFeatureCollection.java  | 196 +++++
 .../internal/geojson/binding/GeoJSONGeometry.java  | 504 +++++++++++++
 .../internal/geojson/binding/GeoJSONObject.java    |  77 ++
 .../org/apache/sis/storage/geojson/Bundle.java     | 212 ++++++
 .../apache/sis/storage/geojson/Bundle.properties   |   5 +
 .../sis/storage/geojson/Bundle_en.properties       |   5 +
 .../sis/storage/geojson/Bundle_fr.properties       |   5 +
 .../sis/storage/geojson/GeoJSONConstants.java      |  59 ++
 .../sis/storage/geojson/GeoJSONFileWriter.java     | 131 ++++
 .../sis/storage/geojson/GeoJSONProvider.java       | 183 +++++
 .../apache/sis/storage/geojson/GeoJSONReader.java  | 356 +++++++++
 .../apache/sis/storage/geojson/GeoJSONStore.java   | 459 ++++++++++++
 .../sis/storage/geojson/GeoJSONStreamWriter.java   | 220 ++++++
 .../apache/sis/storage/geojson/GeoJSONWriter.java  | 469 ++++++++++++
 .../org.apache.sis.storage.DataStoreProvider       |   1 +
 .../storage/geojson/FeatureTypeUtilsTest.java      | 182 +++++
 .../internal/storage/geojson/GeoJSONReadTest.java  | 344 +++++++++
 .../internal/storage/geojson/GeoJSONWriteTest.java | 581 +++++++++++++++
 .../storage/geojson/LiteJsonLocationTest.java      | 112 +++
 .../apache/sis/test/suite/GeoJSONTestSuite.java    |  34 +
 .../sis/internal/storage/geojson/f_prop_array.json |   7 +
 .../sis/internal/storage/geojson/feature.json      |  69 ++
 .../storage/geojson/featurecollection.json         | 158 ++++
 .../internal/storage/geojson/geometries.properties |   8 +
 .../storage/geojson/geometrycollection.json        | 188 +++++
 .../sis/internal/storage/geojson/linestring.json   | 109 +++
 .../sis/internal/storage/geojson/longValue.json    |  24 +
 .../internal/storage/geojson/multilinestring.json  |  85 +++
 .../sis/internal/storage/geojson/multipoint.json   |  13 +
 .../sis/internal/storage/geojson/multipolygon.json | 815 +++++++++++++++++++++
 .../apache/sis/internal/storage/geojson/point.json |   7 +
 .../sis/internal/storage/geojson/polygon.json      | 789 ++++++++++++++++++++
 .../geojson/sample_with_null_properties.json       | 502 +++++++++++++
 41 files changed, 9079 insertions(+), 70 deletions(-)

diff --git a/storage/pom.xml b/storage/pom.xml
index fe99bdb..081c953 100644
--- a/storage/pom.xml
+++ b/storage/pom.xml
@@ -169,6 +169,7 @@
     <module>sis-sqlstore</module>
     <module>sis-netcdf</module>
     <module>sis-geotiff</module>
+    <module>sis-geojson</module>
     <module>sis-earth-observation</module>
     <module>sis-gdal</module>
   </modules>
diff --git a/storage/pom.xml b/storage/sis-geojson/pom.xml
similarity index 57%
copy from storage/pom.xml
copy to storage/sis-geojson/pom.xml
index fe99bdb..ce96135 100644
--- a/storage/pom.xml
+++ b/storage/sis-geojson/pom.xml
@@ -27,7 +27,7 @@
 
   <parent>
     <groupId>org.apache.sis</groupId>
-    <artifactId>parent</artifactId>
+    <artifactId>storage</artifactId>
     <version>2.0-SNAPSHOT</version>
   </parent>
 
@@ -35,12 +35,11 @@
   <!-- ===========================================================
            Module Description
        =========================================================== -->
-  <artifactId>storage</artifactId>
-  <packaging>pom</packaging>
-  <name>Apache SIS storage</name>
+  <groupId>org.apache.sis.storage</groupId>
+  <artifactId>sis-geojson</artifactId>
+  <name>Apache SIS GeoJSON storage</name>
   <description>
-    Group of modules for reading and writing data from/to various storages.
-    Storages are typically file formats or a database schemas.
+    Geojson DataStore implementation.
   </description>
 
 
@@ -72,26 +71,6 @@
     </developer>
   </developers>
   <contributors>
-    <contributor>
-      <name>Thi Phuong Hao Nguyen</name>
-      <email>nguyenthiphuonghao243@gmail.com</email>
-      <organization>VNSC</organization>
-      <organizationUrl>http://vnsc.org.vn</organizationUrl>
-      <timezone>+7</timezone>
-      <roles>
-        <role>developer</role>
-      </roles>
-    </contributor>
-    <contributor>
-      <name>Minh Chinh Vu</name>
-      <email>chinhvm.uet.1995@gmail.com</email>
-      <organization>VNSC</organization>
-      <organizationUrl>http://vnsc.org.vn</organizationUrl>
-      <timezone>+7</timezone>
-      <roles>
-        <role>developer</role>
-      </roles>
-    </contributor>
   </contributors>
 
 
@@ -100,21 +79,18 @@
        =========================================================== -->
   <build>
     <plugins>
-
-      <!-- Compile properties files into resources UTF files and
-           collect JAR files in <root>/target/binaries directory. -->
       <plugin>
-        <groupId>org.apache.sis.core</groupId>
-        <artifactId>sis-build-helper</artifactId>
-        <version>${sis.plugin.version}</version>
-        <executions>
-          <execution>
-            <goals>
-              <goal>compile-resources</goal>
-              <goal>collect-jars</goal>
-            </goals>
-          </execution>
-        </executions>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifestEntries>
+              <Automatic-Module-Name>
+                org.apache.sis.storage.geojson
+              </Automatic-Module-Name>
+            </manifestEntries>
+          </archive>
+        </configuration>
       </plugin>
     </plugins>
   </build>
@@ -125,52 +101,34 @@
        =========================================================== -->
   <dependencies>
     <dependency>
-      <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-metadata</artifactId>
+      <groupId>org.apache.sis.storage</groupId>
+      <artifactId>sis-storage</artifactId>
       <version>${project.version}</version>
     </dependency>
     <dependency>
       <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-referencing</artifactId>
+      <artifactId>sis-cql</artifactId>
       <version>${project.version}</version>
     </dependency>
     <dependency>
-      <groupId>org.opengis</groupId>
-      <artifactId>geoapi-pending</artifactId>
-    </dependency>
-
-    <!-- Test dependencies -->
-    <dependency>
-      <groupId>org.opengis</groupId>
-      <artifactId>geoapi-conformance</artifactId>
+      <groupId>org.locationtech.jts</groupId>
+      <artifactId>jts-core</artifactId>
     </dependency>
     <dependency>
-      <groupId>org.apache.derby</groupId>
-      <artifactId>derby</artifactId>
-      <scope>test</scope>
+      <groupId>com.fasterxml.jackson.core</groupId>
+      <artifactId>jackson-databind</artifactId>
+      <version>2.10.0</version>
     </dependency>
+
     <dependency>
       <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-utility</artifactId>
+      <artifactId>sis-feature</artifactId>
       <version>${project.version}</version>
       <type>test-jar</type>
-      <scope>test</scope>
     </dependency>
   </dependencies>
 
-
-  <!-- ===========================================================
-           Sub-modules included in the build
-       =========================================================== -->
-  <modules>
-    <module>sis-storage</module>
-    <module>sis-shapefile</module>
-    <module>sis-xmlstore</module>
-    <module>sis-sqlstore</module>
-    <module>sis-netcdf</module>
-    <module>sis-geotiff</module>
-    <module>sis-earth-observation</module>
-    <module>sis-gdal</module>
-  </modules>
+  <repositories>
+  </repositories>
 
 </project>
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/FeatureTypeUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/FeatureTypeUtils.java
new file mode 100644
index 0000000..d890837
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/FeatureTypeUtils.java
@@ -0,0 +1,597 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import static java.nio.file.StandardOpenOption.*;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Static;
+import org.apache.sis.util.iso.Names;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.locationtech.jts.geom.Geometry;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.FactoryException;
+import org.opengis.util.GenericName;
+import org.opengis.util.InternationalString;
+import org.opengis.util.NameFactory;
+
+/**
+ * An utility class to handle read/write of FeatureType into a JSON schema file.
+ *
+ * Theses schema are inspired from <a href="http://json-schema.org/">JSON-Schema</a> specification.
+ * Changes are :
+ *  - introducing of a {@code javatype} that define Java class used.
+ *  - introducing of a {@code nillable} property for nullable attributes
+ *  - introducing of a {@code userdata} Map property that contain previous user data information.
+ *  - introducing of a {@code geometry} property to describe a geometry
+ *  - introducing of a {@code crs} property to describe a geometry crs
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class FeatureTypeUtils extends Static {
+
+    private static final String TITLE = "title";
+    private static final String TYPE = "type";
+    private static final String JAVA_TYPE = "javatype";
+    private static final String DESCRIPTION = "description";
+    private static final String PROPERTIES = "properties";
+    private static final String PRIMARY_KEY = "primaryKey";
+    private static final String RESTRICTION = "restriction";
+    private static final String REQUIRED = "required";
+    private static final String MIN_ITEMS = "minItems";
+    private static final String MAX_ITEMS = "maxItems";
+    private static final String USER_DATA = "userdata";
+    private static final String GEOMETRY = "geometry";
+    private static final String GEOMETRY_ATT_NAME = "geometryName";
+    private static final String CRS = "crs";
+
+    private static final String OBJECT = "object";
+    private static final String ARRAY = "array";
+    private static final String INTEGER = "integer";
+    private static final String NUMBER = "number";
+    private static final String STRING = "string";
+    private static final String BOOLEAN = "boolean";
+
+    /**
+     * Write a FeatureType in output File.
+     *
+     * @param ft
+     * @param output
+     * @throws IOException
+     */
+    public static void writeFeatureType(FeatureType ft, Path output) throws IOException, DataStoreException {
+        ArgumentChecks.ensureNonNull("FeatureType", ft);
+        ArgumentChecks.ensureNonNull("outputFile", output);
+
+        final AttributeType<?> geom = GeoJSONUtils
+                .castOrUnwrap(GeoJSONUtils.getDefaultGeometry(ft))
+                .orElseThrow(() -> new DataStoreException("No default Geometry in given FeatureType : " + ft));
+
+        try (OutputStream outStream = Files.newOutputStream(output, CREATE, WRITE, TRUNCATE_EXISTING);
+                JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(outStream, JsonEncoding.UTF8)) {
+
+            writer.useDefaultPrettyPrinter();
+            //start write feature collection.
+            writer.writeStartObject();
+            writer.writeStringField(TITLE, ft.getName().tip().toString());
+            writer.writeStringField(TYPE, OBJECT);
+            writer.writeStringField(JAVA_TYPE, "FeatureType");
+            if (ft.getDescription() != null) {
+                writer.writeStringField(DESCRIPTION, ft.getDescription().toString());
+            }
+
+            writeGeometryType(geom, writer);
+            writeProperties(ft, writer);
+
+            writer.writeEndObject();
+            writer.flush();
+        }
+    }
+
+    public static void writeFeatureTypes(List<FeatureType> fts, OutputStream output) throws IOException, DataStoreException {
+        ArgumentChecks.ensureNonNull("FeatureType", fts);
+        ArgumentChecks.ensureNonNull("outputStream", output);
+
+        if (fts.isEmpty()) {
+            return;
+        }
+
+        if (fts.size() > 1) {
+            JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(output, JsonEncoding.UTF8).useDefaultPrettyPrinter();
+            writer.writeStartArray();
+            for (FeatureType ft : fts) {
+                writeFeatureType(ft, output, writer);
+            }
+            writer.writeEndArray();
+            writer.flush();
+            writer.close();
+        } else {
+            writeFeatureType(fts.get(0), output);
+        }
+    }
+
+    /**
+     * Write a FeatureType in output File.
+     *
+     * @param ft
+     * @param output
+     * @throws IOException
+     */
+    public static void writeFeatureType(FeatureType ft, OutputStream output) throws IOException, DataStoreException {
+        JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(output, JsonEncoding.UTF8).useDefaultPrettyPrinter();
+        writeFeatureType(ft, output, writer);
+        writer.flush();
+        writer.close();
+    }
+
+    private static void writeFeatureType(FeatureType ft, OutputStream output, JsonGenerator writer) throws IOException, DataStoreException {
+        ArgumentChecks.ensureNonNull("FeatureType", ft);
+        ArgumentChecks.ensureNonNull("outputStream", output);
+
+        if (GeoJSONUtils.getDefaultGeometry(ft) == null) {
+            throw new DataStoreException("No default Geometry in given FeatureType : " + ft);
+        }
+
+        //start write feature collection.
+        writer.writeStartObject();
+        writer.writeStringField(TITLE, ft.getName().tip().toString());
+        writer.writeStringField(TYPE, OBJECT);
+        writer.writeStringField(JAVA_TYPE, "FeatureType");
+        if (ft.getDescription() != null) {
+            writer.writeStringField(DESCRIPTION, ft.getDescription().toString());
+        }
+
+        final Optional<AttributeType<?>> geom = GeoJSONUtils.castOrUnwrap(
+                GeoJSONUtils.getDefaultGeometry(ft)
+        );
+        if (geom.isPresent()) {
+            writeGeometryType(geom.get(), writer);
+        }
+
+        writeProperties(ft, writer);
+
+        writer.writeEndObject();
+    }
+
+    private static void writeProperties(FeatureType ft, JsonGenerator writer) throws IOException {
+        writer.writeObjectFieldStart(PROPERTIES);
+
+        Collection<? extends PropertyType> descriptors = ft.getProperties(true);
+        List<String> required = new ArrayList<>();
+
+        for (PropertyType type : descriptors) {
+            boolean isRequired = false;
+
+            if (type instanceof FeatureAssociationRole) {
+                isRequired = writeComplexType((FeatureAssociationRole) type, ((FeatureAssociationRole) type).getValueType(), writer);
+            } else if (type instanceof AttributeType) {
+                if (Geometry.class.isAssignableFrom(((AttributeType) type).getValueClass())) {
+//                    GeometryType geometryType = (GeometryType) type;
+//                    isRequired = writeGeometryType(descriptor, geometryType, writer);
+                } else {
+                    isRequired = writeAttributeType(ft, (AttributeType) type, writer);
+                }
+            }
+            if (isRequired) {
+                required.add(type.getName().tip().toString());
+            }
+        }
+
+        if (!required.isEmpty()) {
+            writer.writeArrayFieldStart(REQUIRED);
+            for (String req : required) {
+                writer.writeString(req);
+            }
+            writer.writeEndArray();
+        }
+        writer.writeEndObject();
+    }
+
+    private static boolean writeComplexType(FeatureAssociationRole descriptor, FeatureType complex, JsonGenerator writer)
+            throws IOException {
+
+        writer.writeObjectFieldStart(descriptor.getName().tip().toString());
+        writer.writeStringField(TYPE, OBJECT);
+        writer.writeStringField(JAVA_TYPE, "ComplexType");
+        if (complex.getDescription() != null) {
+            writer.writeStringField(DESCRIPTION, complex.getDescription().toString());
+        }
+        writer.writeNumberField(MIN_ITEMS, descriptor.getMinimumOccurs());
+        writer.writeNumberField(MAX_ITEMS, descriptor.getMaximumOccurs());
+        writeProperties(complex, writer);
+
+        writer.writeEndObject();
+
+        return descriptor.getMinimumOccurs() > 0;
+    }
+
+    private static boolean writeAttributeType(FeatureType featureType, AttributeType att, JsonGenerator writer)
+            throws IOException {
+
+        writer.writeObjectFieldStart(att.getName().tip().toString());
+        Class binding = att.getValueClass();
+
+        writer.writeStringField(TYPE, findType(binding));
+        writer.writeStringField(JAVA_TYPE, binding.getName());
+        if (att.getDescription() != null) {
+            writer.writeStringField(DESCRIPTION, att.getDescription().toString());
+        }
+        if (GeoJSONUtils.isPartOfPrimaryKey(featureType, att.getName().toString())) {
+            writer.writeBooleanField(PRIMARY_KEY, true);
+        }
+        writer.writeNumberField(MIN_ITEMS, att.getMinimumOccurs());
+        writer.writeNumberField(MAX_ITEMS, att.getMaximumOccurs());
+//        List<Filter> restrictions = att.getRestrictions();
+//        if (restrictions != null && !restrictions.isEmpty()) {
+//            final Filter merged = FF.and(restrictions);
+//            writer.writeStringField(RESTRICTION, CQL.write(merged));
+//        }
+        writer.writeEndObject();
+
+        return att.getMinimumOccurs() > 0;
+    }
+
+    private static String findType(Class binding) {
+
+        if (Integer.class.isAssignableFrom(binding)) {
+            return INTEGER;
+        } else if (Number.class.isAssignableFrom(binding)) {
+            return NUMBER;
+        } else if (Boolean.class.isAssignableFrom(binding)) {
+            return BOOLEAN;
+        } else if (binding.isArray()) {
+            return ARRAY;
+        } else {
+            //fallback
+            return STRING;
+        }
+    }
+
+    private static boolean writeGeometryType(AttributeType geometryType, JsonGenerator writer)
+            throws IOException {
+        writer.writeObjectFieldStart(GEOMETRY);
+        writer.writeStringField(TYPE, OBJECT);
+        if (geometryType.getDescription() != null) {
+            writer.writeStringField(DESCRIPTION, geometryType.getDescription().toString());
+        }
+        writer.writeStringField(JAVA_TYPE, geometryType.getValueClass().getCanonicalName());
+        CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(geometryType);
+        if (crs != null) {
+            final Optional<String> urn = GeoJSONUtils.toURN(crs);
+            if (urn.isPresent()) {
+                writer.writeStringField(CRS, urn.get());
+            }
+        }
+        writer.writeStringField(GEOMETRY_ATT_NAME, geometryType.getName().tip().toString());
+        writer.writeEndObject();
+        return true;
+    }
+
+    /**
+     * Read a FeatureType from an input File.
+     *
+     * @param input file to read
+     * @return FeatureType
+     * @throws IOException
+     */
+    public static FeatureType readFeatureType(Path input) throws IOException, DataStoreException {
+
+        try (InputStream stream = Files.newInputStream(input);
+                JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) {
+
+            final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+            parser.nextToken();
+
+            while (parser.nextToken() != JsonToken.END_OBJECT) {
+
+                final String currName = parser.getCurrentName();
+                switch (currName) {
+                    case TITLE:
+                        ftb.setName(parser.nextTextValue());
+                        break;
+                    case JAVA_TYPE:
+                        String type = parser.nextTextValue();
+                        if (!"FeatureType".equals(type)) {
+                            throw new DataStoreException("Invalid JSON schema : " + input.getFileName().toString());
+                        }
+                        break;
+                    case PROPERTIES:
+                        readProperties(ftb, parser);
+                        break;
+                    case GEOMETRY:
+                        readGeometry(ftb, parser);
+                        break;
+                    case DESCRIPTION:
+                        ftb.setDescription(parser.nextTextValue());
+                        break;
+                }
+            }
+
+            try {
+                return ftb.build();
+            } catch (IllegalStateException ex) {
+                throw new DataStoreException("FeatureType name or default geometry not found in JSON schema\n" + ex.getMessage(), ex);
+            }
+        }
+    }
+
+    private static void readGeometry(FeatureTypeBuilder ftb, JsonParser parser)
+            throws IOException, DataStoreException {
+
+        Class<?> binding = null;
+        CoordinateReferenceSystem crs = null;
+        InternationalString description = null;
+        String geometryName = null;
+
+        parser.nextToken(); // {
+        while (parser.nextToken() != JsonToken.END_OBJECT) { // -> }
+            final String currName = parser.getCurrentName();
+            switch (currName) {
+                case JAVA_TYPE:
+                    String javaTypeValue = parser.nextTextValue();
+                    if (!"ComplexType".equals(javaTypeValue)) {
+                        try {
+                            binding = Class.forName(javaTypeValue);
+                        } catch (ClassNotFoundException e) {
+                            throw new DataStoreException("Geometry javatype " + javaTypeValue + " invalid : " + e.getMessage(), e);
+                        }
+                    }
+                    break;
+
+                case CRS:
+                    String crsCode = parser.nextTextValue();
+                    try {
+                        crs = org.apache.sis.referencing.CRS.forCode(crsCode);
+                    } catch (FactoryException e) {
+                        throw new DataStoreException("Geometry crs " + crsCode + " invalid : " + e.getMessage(), e);
+                    }
+                    break;
+
+                case DESCRIPTION:
+                    description = new SimpleInternationalString(parser.nextTextValue());
+                    break;
+                case GEOMETRY_ATT_NAME:
+                    geometryName = parser.nextTextValue();
+            }
+        }
+
+        if (binding == null) {
+            throw new DataStoreException("Binding class not found.");
+        }
+
+        final GenericName name = geometryName != null ? Names.createLocalName(null, null, geometryName) : Names.createLocalName(null, null, "geometry");
+
+        final AttributeTypeBuilder<?> atb = ftb.addAttribute(binding);
+        atb.setName(name);
+        atb.setDescription(description);
+        if (crs != null) {
+            atb.setCRS(crs);
+        }
+        atb.setMinimumOccurs(1);
+        atb.setMaximumOccurs(1);
+        atb.addRole(AttributeRole.DEFAULT_GEOMETRY);
+    }
+
+    private static void readProperties(FeatureTypeBuilder ftb, JsonParser parser)
+            throws IOException, DataStoreException {
+        parser.nextToken(); // {
+
+        List<String> requiredList = null;
+        while (parser.nextToken() != JsonToken.END_OBJECT) { // -> }
+            final JsonToken currToken = parser.getCurrentToken();
+
+            if (currToken == JsonToken.FIELD_NAME) {
+                final String currName = parser.getCurrentName();
+
+                if (REQUIRED.equals(currName)) {
+                    requiredList = parseRequiredArray(parser);
+                } else {
+                    parseProperty(ftb, parser);
+                }
+            }
+        }
+    }
+
+    private static void parseProperty(FeatureTypeBuilder ftb, JsonParser parser)
+            throws IOException, DataStoreException {
+
+        final String attributeName = parser.getCurrentName();
+        Class<?> binding = String.class;
+        boolean primaryKey = false;
+        int minOccurs = 0;
+        int maxOccurs = 1;
+        CharSequence description = null;
+        String restrictionCQL = null;
+        Map<Object, Object> userData = null;
+        FeatureTypeBuilder subftb = null;
+
+        parser.nextToken();
+        while (parser.nextToken() != JsonToken.END_OBJECT) {
+
+            final String currName = parser.getCurrentName();
+            switch (currName) {
+                case JAVA_TYPE:
+                    String javaTypeValue = parser.nextTextValue();
+                    if (!"ComplexType".equals(javaTypeValue)) {
+                        try {
+                            binding = Class.forName(javaTypeValue);
+                        } catch (ClassNotFoundException e) {
+                            throw new DataStoreException("Attribute " + attributeName + " invalid : " + e.getMessage(), e);
+                        }
+                    }
+                    break;
+                case MIN_ITEMS:
+                    minOccurs = parser.nextIntValue(0);
+                    break;
+                case MAX_ITEMS:
+                    maxOccurs = parser.nextIntValue(1);
+                    break;
+                case PRIMARY_KEY:
+                    primaryKey = parser.nextBooleanValue();
+                    break;
+                case RESTRICTION:
+                    restrictionCQL = parser.nextTextValue();
+                    break;
+                case USER_DATA:
+                    userData = parseUserDataMap(parser);
+                    break;
+                case PROPERTIES:
+                    subftb = new FeatureTypeBuilder();
+                    readProperties(subftb, parser);
+                    break;
+                case DESCRIPTION:
+                    description = parser.nextTextValue();
+                    break;
+            }
+        }
+
+        GenericName name = nameValueOf(attributeName);
+        if (subftb == null) {
+            //build AttributeDescriptor
+            if (binding == null) {
+                throw new DataStoreException("Empty javatype for attribute " + attributeName);
+            }
+
+            AttributeTypeBuilder<?> atb = ftb.addAttribute(binding)
+                    .setName(name)
+                    .setDescription(description)
+                    .setMinimumOccurs(minOccurs)
+                    .setMaximumOccurs(maxOccurs);
+
+            if (primaryKey) {
+                atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+            }
+
+        } else {
+            //build ComplexType
+            subftb.setName(name);
+            subftb.setDescription(description);
+            final FeatureType complexType = subftb.build();
+
+            ftb.addAssociation(complexType)
+                    .setName(name)
+                    .setMinimumOccurs(minOccurs)
+                    .setMaximumOccurs(maxOccurs);
+        }
+    }
+
+    private static Map<Object, Object> parseUserDataMap(JsonParser parser) throws IOException {
+
+        Map<Object, Object> map = new HashMap<>();
+        parser.nextToken(); // {
+        while (parser.nextToken() != JsonToken.END_OBJECT) {
+            Object key = parser.getCurrentName();
+            JsonToken next = parser.nextToken();
+            map.put(key, GeoJSONParser.getValue(next, parser));
+        }
+        return map;
+
+    }
+
+    private static List<String> parseRequiredArray(JsonParser parser) throws IOException {
+        List<String> requiredList = new ArrayList<>();
+        parser.nextToken(); // [
+
+        while (parser.nextToken() != JsonToken.END_ARRAY) { // -> ]
+            requiredList.add(parser.getValueAsString());
+        }
+
+        return requiredList;
+    }
+
+    /**
+     * Parse a string value that can be expressed in 2 different forms :
+     * JSR-283 extended form : {uri}localpart
+     * Separator form : uri:localpart
+     *
+     * if the given string do not match any, then a Name with no namespace will
+     * be created and the localpart will be the given string.
+     */
+    public static GenericName nameValueOf(final String candidate) {
+
+        if (candidate.startsWith("{")) {
+            //name is in extended form
+            return toSessionNamespaceFromExtended(candidate);
+        }
+
+        int index = candidate.lastIndexOf(':');
+
+        if (index <= 0) {
+            return createName(null, candidate);
+        } else {
+            final String uri = candidate.substring(0, index);
+            final String name = candidate.substring(index + 1, candidate.length());
+            return createName(uri, name);
+        }
+
+    }
+
+    private static GenericName toSessionNamespaceFromExtended(final String candidate) {
+        final int index = candidate.indexOf('}');
+
+        if (index == -1) {
+            throw new IllegalArgumentException("Invalide extended form : " + candidate);
+        }
+
+        final String uri = candidate.substring(1, index);
+        final String name = candidate.substring(index + 1, candidate.length());
+
+        return createName(uri, name);
+    }
+
+    /**
+     *
+     * @param namespace if null or empty will not be used for the name
+     * @param local mandatory
+     */
+    public static GenericName createName(final String namespace, final String local) {
+
+        // WARNING: DefaultFactories.NAMES is not a public API and may change in any future SIS version.
+        if (namespace == null || namespace.isEmpty()) {
+            return DefaultFactories.forBuildin(NameFactory.class).createGenericName(null, local);
+        } else {
+            return DefaultFactories.forBuildin(NameFactory.class).createGenericName(null, namespace, local);
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONParser.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONParser.java
new file mode 100644
index 0000000..b7eecae
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONParser.java
@@ -0,0 +1,691 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson;
+
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry;
+import org.apache.sis.internal.geojson.binding.GeoJSONCRS;
+import org.apache.sis.internal.geojson.binding.GeoJSONObject;
+import org.apache.sis.internal.geojson.binding.GeoJSONFeatureCollection;
+import org.apache.sis.internal.geojson.binding.GeoJSONFeature;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import static org.apache.sis.storage.geojson.GeoJSONConstants.*;
+
+/**
+ * Efficient GeoJSONParsing using jackson {@link JsonParser}
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONParser {
+
+    public static final JsonFactory FACTORY = new JsonFactory();
+    public static final Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson.utils");
+
+    private GeoJSONParser() {}
+
+    /**
+     * Parse a json file and return a GeoJSONObject. If parser was construct
+     * with lazyParsing as {@code true} and root object is a FeatureCollection,
+     * returned GeoJSONFeatureCollection will only have start and end feature
+     * array location. Otherwise, all Feature will be parsed and add to
+     * GeoJSONFeatureCollection.
+     *
+     * @param jsonFile file to parse
+     * @return GeoJSONObject
+     * @throws IOException
+     */
+    public static GeoJSONObject parse(Path jsonFile) throws IOException {
+        return parse(jsonFile, Boolean.FALSE);
+    }
+
+    /**
+     * Parse a json file and return a GeoJSONObject. If parser was construct
+     * with lazyParsing as {@code true} and root object is a FeatureCollection,
+     * returned GeoJSONFeatureCollection will only have start and end feature
+     * array location. Otherwise, all Feature will be parsed and add to
+     * GeoJSONFeatureCollection.
+     *
+     * @param jsonFile file to parse
+     * @param lazy lazy mode flag
+     * @return GeoJSONObject
+     * @throws IOException
+     */
+    public static GeoJSONObject parse(Path jsonFile, boolean lazy) throws IOException {
+
+        try (InputStream reader = Files.newInputStream(jsonFile);
+                JsonParser p = FACTORY.createParser(reader)) {
+
+            JsonToken startToken = p.nextToken();
+            assert (startToken == JsonToken.START_OBJECT) : "Input File is not a JSON file " + jsonFile.toAbsolutePath().toString();
+            return parseGeoJSONObject(p, lazy, jsonFile);
+        }
+    }
+
+    /**
+     * Parse a json InputStream and return a GeoJSONObject. In InputStream case,
+     * lazy loading of FeatureCollection is disabled.
+     *
+     * @param inputStream stream to parse
+     * @return GeoJSONObject
+     * @throws IOException
+     */
+    public static GeoJSONObject parse(InputStream inputStream) throws IOException {
+        try (JsonParser p = FACTORY.createParser(inputStream)) {
+            JsonToken startToken = p.nextToken();
+            assert (startToken == JsonToken.START_OBJECT) : "Input stream is not a valid JSON ";
+            return parseGeoJSONObject(p);
+        }
+    }
+
+    /**
+     * Parse a GeoJSONObject (FeatureCollection, Feature or a Geometry)
+     * JsonParser location MUST be on a START_OBJECT token.
+     *
+     * @param p parser jackson parser with current token on a START_OBJECT.
+     * @return GeoJSONObject (FeatureCollection, Feature or a Geometry)
+     * @throws IOException
+     */
+    public static GeoJSONObject parseGeoJSONObject(JsonParser p) throws IOException {
+        return parseGeoJSONObject(p, Boolean.FALSE, null);
+    }
+
+    /**
+     * Parse a GeoJSONObject (FeatureCollection, Feature or a Geometry)
+     * JsonParser location MUST be on a START_OBJECT token.
+     *
+     * @param p parser jackson parser with current token on a START_OBJECT.
+     * @param lazy lazy mode flag
+     * @return GeoJSONObject (FeatureCollection, Feature or a Geometry)
+     * @throws IOException
+     */
+    private static GeoJSONObject parseGeoJSONObject(JsonParser p, Boolean lazy, Path source) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_OBJECT);
+
+        GeoJSONObject object = new GeoJSONObject();
+        while (p.nextToken() != JsonToken.END_OBJECT) {
+            String fieldname = p.getCurrentName();
+
+            if (fieldname == null) {
+                throw new IOException("Parsing error, expect object field name value but got null");
+            }
+
+            switch (fieldname) {
+                case ID:
+                    p.nextToken();
+                    String id = p.getValueAsString();
+                    if (object instanceof GeoJSONFeature) {
+                        ((GeoJSONFeature) object).setId(id);
+                    }
+                    break;
+                case TYPE:
+                    p.nextToken();
+                    String value = p.getValueAsString();
+                    object = getOrCreateFromType(object, value, lazy);
+                    break;
+
+                case BBOX:
+                    //array
+                    p.nextToken(); // "["
+                    object.setBbox(parseBBoxArray(p));
+                    break;
+
+                case CRS:
+                    //object
+                    p.nextToken(); // "{"
+                    object.setCrs(parseCRS(p));
+                    break;
+
+                case FEATURES:
+                    object = getOrCreateFromType(object, FEATURE_COLLECTION, lazy);
+
+                    //array of GeoJSONFeature
+                    if (Boolean.TRUE.equals(lazy)) {
+                        lazyParseFeatureCollection((GeoJSONFeatureCollection) object, p, source);
+                    } else {
+                        parseFeatureCollection((GeoJSONFeatureCollection) object, p);
+                    }
+                    break;
+
+                case PROPERTIES:
+                    object = getOrCreateFromType(object, FEATURE);
+                    //object
+                    parseProperties((GeoJSONFeature) object, p);
+                    break;
+
+                case GEOMETRY:
+                    object = getOrCreateFromType(object, FEATURE);
+                    //object
+                    parseFeatureGeometry((GeoJSONFeature) object, p);
+                    break;
+
+                case COORDINATES:
+                    //array
+                    if (object instanceof GeoJSONGeometry) {
+                        parseGeometry((GeoJSONGeometry) object, p);
+                    } else {
+                        LOGGER.log(Level.WARNING, "Error need type before coordinates");
+                    }
+                    break;
+
+                case GEOMETRIES:
+                    if (object instanceof GeoJSONGeometryCollection) {
+                        object = getOrCreateFromType(object, GEOMETRY_COLLECTION);
+                        //array of GeoJSONGeometry
+                        parseGeometryCollection((GeoJSONGeometryCollection) object, p);
+                    } else {
+                        LOGGER.log(Level.WARNING, "Error need type before coordinates");
+                    }
+                    break;
+                default:
+                    if (p.getCurrentToken() == JsonToken.START_OBJECT) {
+                        //skip any unknown properties
+                        parseGeoJSONObject(p, lazy, source);
+                    }
+                    break;
+            }
+        }
+        return object;
+    }
+
+    /**
+     * Parse properties map and add to given Feature
+     *
+     * @param feature Feature to attach properties
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseProperties(GeoJSONFeature feature, JsonParser p) throws IOException {
+        p.nextToken(); // "{"
+        feature.getProperties().putAll(parseMap(p));
+    }
+
+    /**
+     * Parse a map of String, Object. JsonParser location MUST be on a
+     * START_OBJECT token.
+     *
+     * @param p parser jackson parser with current token on a START_OBJECT.
+     * @return Map of String - Object
+     * @throws IOException
+     */
+    private static Map<String, Object> parseMap(JsonParser p) throws IOException {
+        Map<String, Object> map = new HashMap<>();
+        final JsonToken currentToken = p.getCurrentToken();
+        if (currentToken == JsonToken.VALUE_NULL) {
+            return map;
+        }
+
+        if (currentToken != JsonToken.START_OBJECT) {
+            LOGGER.log(Level.WARNING, "Expect START_OBJECT token but got " + currentToken + " for " + p.getCurrentName());
+            return map;
+        }
+
+        assert (currentToken == JsonToken.START_OBJECT);
+        while (p.nextToken() != JsonToken.END_OBJECT) {
+            String key = p.getCurrentName();
+            JsonToken next = p.nextToken();
+
+            map.put(key, getValue(next, p));
+        }
+        return map;
+    }
+
+    /**
+     * Parse a List of Objects. JsonParser location MUST be on a START_ARRAY
+     * token.
+     *
+     * @param p parser jackson parser with current token on a START_ARRAY.
+     * @return List of Objects
+     * @throws IOException
+     */
+    private static List<Object> parseArray(JsonParser p) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<Object> list = new ArrayList<>();
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            list.add(getValue(p.getCurrentToken(), p));
+        }
+        return list;
+    }
+
+    /**
+     * Parse a List of Objects. JsonParser location MUST be on a START_ARRAY
+     * token.
+     *
+     * @param p parser jackson parser with current token on a START_ARRAY.
+     * @return array object typed after first element class
+     * @throws IOException
+     */
+    private static Object parseArray2(JsonParser p) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<Object> list = new ArrayList<>();
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            list.add(getValue(p.getCurrentToken(), p));
+        }
+
+        if (list.isEmpty()) {
+            return new Object[0];
+        }
+
+        Class binding = list.get(0).getClass();
+        Object newArray = Array.newInstance(binding, list.size());
+        for (int i = 0; i < list.size(); i++) {
+            Array.set(newArray, i, list.get(i));
+        }
+
+        return newArray;
+    }
+
+    /**
+     * Convert the current token to appropriate object. Supported (String,
+     * Integer, Float, Boolean, Null, Array, Map)
+     *
+     * @param token current token
+     * @param p parser
+     * @return current token value String or Integer or Float or Boolean or null
+     * or an array or a map.
+     * @throws IOException
+     */
+    static Object getValue(JsonToken token, JsonParser p) throws IOException {
+        if (token == JsonToken.VALUE_STRING) {
+            return p.getValueAsString();
+        } else if (token == JsonToken.VALUE_NUMBER_INT) {
+            final long value = p.getValueAsLong();
+            if (value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE) {
+                return (int) value;
+            }
+            return value;
+        } else if (token == JsonToken.VALUE_NUMBER_FLOAT) {
+            return p.getValueAsDouble();
+        } else if (token == JsonToken.VALUE_TRUE || token == JsonToken.VALUE_FALSE) {
+            return token == JsonToken.VALUE_TRUE;
+        } else if (token == JsonToken.VALUE_NULL) {
+            return null;
+        } else if (token == JsonToken.START_ARRAY) {
+            return parseArray2(p);
+        } else if (token == JsonToken.START_OBJECT) {
+            return parseMap(p);
+        } else {
+            throw new UnsupportedOperationException("Unsupported JSON token : " + token + ", value : " + p.getText());
+        }
+    }
+
+    /**
+     * Parse a coordinates array(s) and add to given GeoJSONGeometry.
+     *
+     * @param geom GeoJSONGeometry
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseGeometry(GeoJSONGeometry geom, JsonParser p) throws IOException {
+        p.nextToken(); // "["
+        if (geom instanceof GeoJSONPoint) {
+            ((GeoJSONPoint) geom).setCoordinates(parsePoint(p));
+        } else if (geom instanceof GeoJSONLineString) {
+            ((GeoJSONLineString) geom).setCoordinates(parseLineString(p));
+        } else if (geom instanceof GeoJSONPolygon) {
+            ((GeoJSONPolygon) geom).setCoordinates(parseMultiLineString(p));
+        } else if (geom instanceof GeoJSONMultiPoint) {
+            ((GeoJSONMultiPoint) geom).setCoordinates(parseLineString(p));
+        } else if (geom instanceof GeoJSONMultiLineString) {
+            ((GeoJSONMultiLineString) geom).setCoordinates(parseMultiLineString(p));
+        } else if (geom instanceof GeoJSONMultiPolygon) {
+            ((GeoJSONMultiPolygon) geom).setCoordinates(parseMultiPolygon(p));
+        }
+    }
+
+    /**
+     * Full parse of GeoJSONFeatures for a GeoJSONFeatureCollection
+     *
+     * @param coll GeoJSONFeatureCollection
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseFeatureCollection(GeoJSONFeatureCollection coll, JsonParser p) throws IOException {
+        p.nextToken(); // "{"
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+
+            GeoJSONObject obj = parseGeoJSONObject(p, false, null);
+            if (obj instanceof GeoJSONFeature) {
+                coll.getFeatures().add((GeoJSONFeature) obj);
+            } else {
+                LOGGER.log(Level.WARNING, "ERROR feature collection");
+            }
+        }
+    }
+
+    /**
+     * Lazy parse of GeoJSONFeatures for a GeoJSONFeatureCollection. Only find
+     * an set START_ARRAY and END_ARRAY TokenLocation of the features array to
+     * the GeoJSONFeatureCollection object.
+     *
+     * @param coll GeoJSONFeatureCollection
+     * @param p parser
+     * @param source
+     * @throws IOException
+     */
+    private static void lazyParseFeatureCollection(GeoJSONFeatureCollection coll, JsonParser p, Path source) throws IOException {
+
+        p.nextToken();
+        coll.setSourceInput(source);
+        coll.setStartPosition(p.getCurrentLocation());
+
+        int startArray = 1;
+        int endArray = 0;
+
+        //loop to the right "]"
+        while (startArray != endArray) {
+            JsonToken token = p.nextToken();
+            if (token == JsonToken.START_ARRAY) {
+                startArray++;
+            }
+            if (token == JsonToken.END_ARRAY) {
+                endArray++;
+            }
+        }
+
+        coll.setEndPosition(p.getCurrentLocation());
+    }
+
+    /**
+     * Parse GeoJSONGeometry for GeoJSONFeature.
+     *
+     * @param feature GeoJSONFeature
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseFeatureGeometry(GeoJSONFeature feature, JsonParser p) throws IOException {
+        p.nextToken(); // "{"
+        GeoJSONObject obj = parseGeoJSONObject(p);
+        assert (obj != null) : "Un-parsable GeoJSONGeometry.";
+        assert (obj instanceof GeoJSONGeometry) : "Unexpected GeoJSONObject : " + obj.getType() + " expected : GeoJSONGeometry";
+        feature.setGeometry((GeoJSONGeometry) obj);
+    }
+
+    /**
+     * Parse GeoJSONGeometry for GeoJSONGeometryCollection.
+     *
+     * @param geom GeoJSONGeometryCollection
+     * @param p parser
+     * @throws IOException
+     */
+    private static void parseGeometryCollection(GeoJSONGeometryCollection geom, JsonParser p) throws IOException {
+        p.nextToken(); // "["
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            GeoJSONObject obj = parseGeoJSONObject(p);
+            assert (obj != null) : "Un-parsable GeoJSONGeometry.";
+            assert (obj instanceof GeoJSONGeometry) : "Unexpected GeoJSONObject : " + obj.getType() + " expected : GeoJSONGeometry";
+            geom.getGeometries().add((GeoJSONGeometry) obj);
+        }
+    }
+
+    /**
+     * Create GeoJSONObject using type. If previous object is not null, forward
+     * bbox and crs parameter to the new GeoJSONObject.
+     *
+     * @param object previous object.
+     * @param type see {@link org.apache.sis.storage.geojson.GeoJSONConstants}
+     * @return GeoJSONObject
+     */
+    private static GeoJSONObject getOrCreateFromType(GeoJSONObject object, String type) {
+        return getOrCreateFromType(object, type, Boolean.FALSE);
+
+    }
+
+    /**
+     * Create GeoJSONObject using type. If previous object is not null, forward
+     * bbox and crs parameter to the new GeoJSONObject.
+     *
+     * @param object previous object.
+     * @param type see {@link org.apache.sis.storage.geojson.GeoJSONConstants}
+     * @param lazy lazy mode flag
+     * @return GeoJSONObject
+     */
+    private static GeoJSONObject getOrCreateFromType(GeoJSONObject object, String type, Boolean lazy) {
+
+        GeoJSONObject result = object;
+        if (type != null) {
+            switch (type) {
+                case FEATURE_COLLECTION:
+                    if (result instanceof GeoJSONFeatureCollection) {
+                        return result;
+                    } else {
+                        result = new GeoJSONFeatureCollection(lazy);
+                    }
+                    break;
+                case FEATURE:
+                    if (result instanceof GeoJSONFeature) {
+                        return result;
+                    } else {
+                        result = new GeoJSONFeature();
+                    }
+                    break;
+                case POINT:
+                    if (result instanceof GeoJSONPoint) {
+                        return result;
+                    } else {
+                        result = new GeoJSONPoint();
+                    }
+                    break;
+                case LINESTRING:
+                    if (result instanceof GeoJSONLineString) {
+                        return result;
+                    } else {
+                        result = new GeoJSONLineString();
+                    }
+                    break;
+                case POLYGON:
+                    if (result instanceof GeoJSONPolygon) {
+                        return result;
+                    } else {
+                        result = new GeoJSONPolygon();
+                    }
+                    break;
+                case MULTI_POINT:
+                    if (result instanceof GeoJSONMultiPoint) {
+                        return result;
+                    } else {
+                        result = new GeoJSONMultiPoint();
+                    }
+                    break;
+                case MULTI_LINESTRING:
+                    if (result instanceof GeoJSONMultiLineString) {
+                        return result;
+                    } else {
+                        result = new GeoJSONMultiLineString();
+                    }
+                    break;
+                case MULTI_POLYGON:
+                    if (result instanceof GeoJSONMultiPolygon) {
+                        return result;
+                    } else {
+                        result = new GeoJSONMultiPolygon();
+                    }
+                    break;
+                case GEOMETRY_COLLECTION:
+                    if (result instanceof GeoJSONGeometryCollection) {
+                        return result;
+                    } else {
+                        result = new GeoJSONGeometryCollection();
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown type " + type);
+            }
+
+            if (object != null) {
+                result.setBbox(object.getBbox());
+                result.setCrs(object.getCrs());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Parse the bbox array. JsonParser location MUST be on a START_ARRAY token.
+     *
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double with a length of 4 or 6.
+     * @throws IOException
+     */
+    private static double[] parseBBoxArray(JsonParser p) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_ARRAY);
+        double[] bbox = new double[4];
+
+        int idx = 0;
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            if (idx == 4) {
+                bbox = Arrays.copyOf(bbox, 6);
+            }
+            bbox[idx++] = p.getDoubleValue();
+        }
+
+        return bbox;
+    }
+
+    /**
+     * Parse a CRS Object. JsonParser location MUST be on a START_OBJECT token.
+     *
+     * @param p JsonParser location MUST be on a START_OBJECT token.
+     * @return GeoJSONCRS
+     * @throws IOException
+     */
+    private static GeoJSONCRS parseCRS(JsonParser p) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_OBJECT);
+        GeoJSONCRS crs = new GeoJSONCRS();
+
+        while (p.nextToken() != JsonToken.END_OBJECT) {
+            String fieldName = p.getCurrentName();
+            if (TYPE.equals(fieldName)) {
+                crs.setType(p.nextTextValue());
+            } else if (PROPERTIES.equals(fieldName)) {
+                //object
+                p.nextToken();
+                while (p.nextToken() != JsonToken.END_OBJECT) {
+                    crs.getProperties().put(p.getCurrentName(), p.getValueAsString());
+                }
+            }
+        }
+
+        return crs;
+    }
+
+    /**
+     * Parse a Coordinate. JsonParser location MUST be on a START_ARRAY token.
+     *
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double like [X,Y,(Z)]
+     * @throws IOException
+     */
+    private static double[] parsePoint(JsonParser p) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_ARRAY);
+        double[] pt = new double[2];
+
+        int idx = 0;
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            if (idx == 2) {
+                pt = Arrays.copyOf(pt, 3);
+            }
+            pt[idx++] = p.getDoubleValue();
+        }
+        return pt;
+    }
+
+    /**
+     * Parse a LineString/MultiPoint. JsonParser location MUST be on a
+     * START_ARRAY token.
+     *
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double like [[X0,Y0,(Z0)], [X1,Y1,(Z1)]]
+     * @throws IOException
+     */
+    private static double[][] parseLineString(JsonParser p) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<double[]> line = new ArrayList<>();
+
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            line.add(parsePoint(p));
+        }
+        return line.toArray(new double[line.size()][]);
+    }
+
+    /**
+     * Parse a List of LineString or Polygon. JsonParser location MUST be on a
+     * START_ARRAY token.
+     *
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double like [ [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+     * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]], ... ]
+     * @throws IOException
+     */
+    private static double[][][] parseMultiLineString(JsonParser p) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<double[][]> lines = new ArrayList<>();
+
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            lines.add(parseLineString(p));
+        }
+        return lines.toArray(new double[lines.size()][][]);
+    }
+
+    /**
+     * Parse a List of Polygons (list of list of LineString). JsonParser
+     * location MUST be on a START_ARRAY token.
+     *
+     * @param p JsonParser location MUST be on a START_ARRAY token.
+     * @return an array of double like [[ [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+     * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]] ],[ [[X0,Y0,(Z0)], [X1,Y1,(Z1)]],
+     * [[X0,Y0,(Z0)], [X1,Y1,(Z1)]] ], ... ]
+     * @throws IOException
+     */
+    private static double[][][][] parseMultiPolygon(JsonParser p) throws IOException {
+        assert (p.getCurrentToken() == JsonToken.START_ARRAY);
+        List<double[][][]> polygons = new ArrayList<>();
+
+        // messages is array, loop until token equal to "]"
+        while (p.nextToken() != JsonToken.END_ARRAY) {
+            polygons.add(parseMultiLineString(p));
+        }
+        return polygons.toArray(new double[polygons.size()][][][]);
+    }
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONUtils.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONUtils.java
new file mode 100644
index 0000000..01246e0
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/GeoJSONUtils.java
@@ -0,0 +1,580 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonLocation;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.io.wkt.Convention;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.internal.geojson.binding.GeoJSONObject;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.FactoryException;
+
+import java.io.*;
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.text.ParseException;
+import java.util.Collection;
+import java.util.logging.Level;
+
+import org.apache.sis.util.Utilities;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TimeZone;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import org.apache.sis.feature.AbstractOperation;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.io.wkt.WKTFormat;
+import org.apache.sis.metadata.iso.citation.Citations;
+import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.util.Numbers;
+import static org.apache.sis.storage.geojson.GeoJSONConstants.*;
+import org.apache.sis.util.Static;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.IdentifiedType;
+import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.feature.PropertyType;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONUtils extends Static {
+
+    /**
+     * Fallback CRS
+     */
+    private static final CoordinateReferenceSystem DEFAULT_CRS = CommonCRS.WGS84.normalizedGeographic();
+
+    /**
+     * A test to know if a given property is an SIS convention or not. Return true if
+     * the property is NOT marked as an SIS convention, false otherwise.
+     */
+    public static final Predicate<IdentifiedType> IS_NOT_CONVENTION = p -> !AttributeConvention.contains(p.getName());
+
+    /**
+     * Extract the coordinate reference system associated to the primary geometry
+     * of input data type.
+     *
+     * @implNote
+     * Primary geometry is determined using {@link #getDefaultGeometry(org.opengis.feature.FeatureType) }.
+     *
+     * @param type The data type to extract reference system from.
+     * @return The CRS associated to the default geometry of this data type, or
+     * a null value if we cannot determine what is the primary geometry of the
+     * data type. Note that a null value is also returned if a geometry property
+     * is found, but no CRS characteristics is associated with it.
+     */
+    public static CoordinateReferenceSystem getCRS(FeatureType type){
+        try {
+            return getCRS(getDefaultGeometry(type));
+        } catch (IllegalArgumentException | IllegalStateException ex) {
+            //no default geometry property
+            return null;
+        }
+    }
+
+    /**
+     * Extract CRS characteristic if it exist.
+     *
+     * @param type
+     * @return CoordinateReferenceSystem or null
+     */
+    public static CoordinateReferenceSystem getCRS(PropertyType type){
+        return getCharacteristicValue(type, AttributeConvention.CRS_CHARACTERISTIC.toString(), null);
+    }
+
+    /**
+     * Extract characteristic value if it exist.
+     *
+     * @param <T> expected value class
+     * @param type base type to search in
+     * @param charName characteristic name
+     * @param defaulValue default value if characteristic is missing or null.
+     * @return characteristic value or default value is not found
+     */
+    public static <T> T getCharacteristicValue(PropertyType type, String charName, T defaulValue){
+        while (type instanceof Operation) {
+            type = (PropertyType) ((Operation) type).getResult();
+        }
+        if (type instanceof AttributeType) {
+            final AttributeType at = (AttributeType) ((AttributeType) type).characteristics().get(charName);
+            if (at != null) {
+                T val = (T) at.getDefaultValue();
+                return (val == null) ? defaulValue : val;
+            }
+        }
+        return defaulValue;
+    }
+
+    /**
+     * Search for the main geometric property in the given type. We'll search
+     * for an SIS convention first (see
+     * {@link AttributeConvention#GEOMETRY_PROPERTY}. If no convention is set on
+     * the input type, we'll check if it contains a single geometric property.
+     * If it's the case, we return it. Otherwise (no or multiple geometries), we
+     * throw an exception.
+     *
+     * @param type The data type to search into.
+     * @return The main geometric property we've found.
+     * @throws PropertyNotFoundException If no geometric property is available
+     * in the given type.
+     * @throws IllegalStateException If no convention is set (see
+     * {@link AttributeConvention#GEOMETRY_PROPERTY}), and we've found more than
+     * one geometry.
+     */
+    public static PropertyType getDefaultGeometry(final FeatureType type) throws PropertyNotFoundException, IllegalStateException {
+        PropertyType geometry;
+        try {
+            geometry = type.getProperty(AttributeConvention.GEOMETRY_PROPERTY.toString());
+        } catch (PropertyNotFoundException e) {
+            try {
+                geometry = searchForGeometry(type);
+            } catch (RuntimeException e2) {
+                e2.addSuppressed(e);
+                throw e2;
+            }
+        }
+
+        return geometry;
+    }
+
+    /**
+     * Search for a geometric attribute outside SIS conventions. More accurately,
+     * we expect the given type to have a single geometry attribute. If many are
+     * found, an exception is thrown.
+     *
+     * @param type The data type to search into.
+     * @return The only geometric property we've found.
+     * @throws PropertyNotFoundException If no geometric property is available in
+     * the given type.
+     * @throws IllegalStateException If we've found more than one geometry.
+     */
+    private static PropertyType searchForGeometry(final FeatureType type) throws PropertyNotFoundException, IllegalStateException {
+        final List<? extends PropertyType> geometries = type.getProperties(true).stream()
+                .filter(IS_NOT_CONVENTION)
+                .filter(AttributeConvention::isGeometryAttribute)
+                .collect(Collectors.toList());
+
+        if (geometries.size() < 1) {
+            throw new PropertyNotFoundException("No geometric property can be found outside of sis convention.");
+        } else if (geometries.size() > 1) {
+            throw new IllegalStateException("Multiple geometries found. We don't know which one to select.");
+        } else {
+            return geometries.get(0);
+        }
+    }
+
+    /**
+     * Get main geometry property value. The ways this method determines default
+     * geometry property are the same as {@link #getDefaultGeometry(org.opengis.feature.FeatureType) }.
+     *
+     * @param input the feature to extract geometry from.
+     * @return Value of the main geometric property of the given feature. The returned
+     * optional will be empty only if the feature defines a geometric property, but has
+     * no value for it.
+     * @throws PropertyNotFoundException If no geometric property is available in
+     * the given feature.
+     * @throws IllegalStateException If we've found more than one geometry.
+     */
+    public static Optional<Object> getDefaultGeometryValue(Feature input) throws PropertyNotFoundException, IllegalStateException {
+        Object geometry;
+        try {
+            geometry = input.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString());
+        } catch (PropertyNotFoundException ex) {
+            try {
+                final PropertyType geomType = getDefaultGeometry(input.getType());
+                geometry = input.getPropertyValue(geomType.getName().toString());
+            } catch (RuntimeException e) {
+                e.addSuppressed(ex);
+                throw e;
+            }
+        }
+
+        return Optional.ofNullable(geometry);
+    }
+
+    /**
+     * Parse LinkedCRS (href + type).
+     * @param href
+     * @param crsType
+     * @return CoordinateReferenceSystem or null.
+     */
+    public static CoordinateReferenceSystem parseCRS(String href, String crsType) {
+        String wkt = null;
+        try (InputStream stream = new URL(href).openStream()) {
+            wkt = IOUtilities.toString(stream);
+        } catch (IOException e) {
+            GeoJSONParser.LOGGER.log(Level.WARNING, "Can't access to linked CRS "+href, e);
+        }
+
+        if (wkt != null) {
+            WKTFormat format = new WKTFormat(Locale.ENGLISH, TimeZone.getTimeZone("GMT"));
+            if (crsType.equals(CRS_TYPE_OGCWKT)) {
+                format.setConvention(Convention.WKT1);
+            } else if (crsType.equals(CRS_TYPE_ESRIWKT)) {
+                format.setConvention(Convention.WKT1_COMMON_UNITS);
+            }
+            try {
+                Object obj = format.parseObject(wkt);
+                if (obj instanceof CoordinateReferenceSystem) {
+                    return (CoordinateReferenceSystem) obj;
+                } else {
+                    GeoJSONParser.LOGGER.log(Level.WARNING, "Parsed WKT is not a CRS "+wkt);
+                }
+            } catch (ParseException e) {
+                GeoJSONParser.LOGGER.log(Level.WARNING, "Can't parse CRS WKT " + crsType+ " : "+wkt, e);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Test if given data type is an attribute as defined by {@link AttributeType},
+     * or if it depends on an attribute, and return it (the attribute) if possible.
+     * @param input the data type to unravel the attribute from.
+     * @return The found attribute or an empty shell if we cannot find any.
+     */
+    public static Optional<AttributeType<?>> castOrUnwrap(IdentifiedType input) {
+        // In case an operation also implements attribute type, we check it first.
+        // TODO : cycle detection ?
+        while (!(input instanceof AttributeType) && input instanceof Operation) {
+            input = ((Operation) input).getResult();
+        }
+
+        if (input instanceof AttributeType) {
+            return Optional.of((AttributeType) input);
+        }
+
+        return Optional.empty();
+    }
+
+    /**
+     * Returns true if property is a component of the feature type primary key.
+     */
+    public static boolean isPartOfPrimaryKey(FeatureType type, String propertyName) {
+        PropertyType property;
+        try {
+            property = type.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString());
+        } catch (PropertyNotFoundException ex) {
+            //no identifier property
+            return false;
+        }
+        if (property instanceof AbstractOperation) {
+            final Set<String> dependencies = ((AbstractOperation) property).getDependencies();
+            return dependencies.contains(propertyName);
+        }
+        return false;
+    }
+
+    /**
+     * Convert a CoordinateReferenceSystem to a identifier string like
+     * urn:ogc:def:crs:EPSG::4326
+     * @param crs
+     * @return
+     */
+    public static Optional<String> toURN(CoordinateReferenceSystem crs) {
+        ArgumentChecks.ensureNonNull("crs", crs);
+
+        String urn = null;
+        try {
+            if (Utilities.equalsIgnoreMetadata(crs, CommonCRS.WGS84.normalizedGeographic())) {
+                crs = CommonCRS.WGS84.normalizedGeographic();
+            }
+
+            urn = IdentifiedObjects.lookupURN(crs, Citations.EPSG);
+        } catch (FactoryException e) {
+            GeoJSONParser.LOGGER.log(Level.WARNING, "Unable to extract epsg code from given CRS "+crs, e);
+        }
+
+        return Optional.ofNullable(urn);
+    }
+
+    /**
+     * Try to extract/parse the CoordinateReferenceSystem from a GeoJSONObject.
+     * Use WGS_84 as fallback CRS.
+     * @param obj GeoJSONObject
+     * @return GeoJSONObject CoordinateReferenceSystem or fallback CRS (WGS84).
+     * @throws MalformedURLException
+     * @throws DataStoreException
+     */
+    public static CoordinateReferenceSystem getCRS(GeoJSONObject obj) throws MalformedURLException, DataStoreException {
+        CoordinateReferenceSystem crs = null;
+        try {
+            if (obj.getCrs() != null) {
+                crs = obj.getCrs().getCRS();
+            }
+        } catch (FactoryException e) {
+            throw new DataStoreException(e.getMessage(), e);
+        }
+
+        if (crs == null) {
+            crs = DEFAULT_CRS;
+        }
+        return crs;
+    }
+
+    /**
+     * Utility method Create geotk Envelope if bbox array is filled.
+     * @return Envelope or null.
+     */
+    public static Envelope getEnvelope(GeoJSONObject obj, CoordinateReferenceSystem crs) {
+
+        double[] bbox = obj.getBbox();
+        if (bbox != null) {
+            GeneralEnvelope env = new GeneralEnvelope(crs);
+            int dim = bbox.length/2;
+            if (dim == 2) {
+                env.setRange(0, bbox[0], bbox[2]);
+                env.setRange(1, bbox[1], bbox[3]);
+            } else if (dim == 3) {
+                env.setRange(0, bbox[0], bbox[3]);
+                env.setRange(1, bbox[1], bbox[4]);
+            }
+            return env;
+        }
+        return null;
+    }
+
+    /**
+     * Return file name without extension
+     * @param file candidate
+     * @return String
+     */
+    public static String getNameWithoutExt(Path file) {
+        return IOUtilities.filenameWithoutExtension(file.toUri().toString());
+    }
+
+    /**
+     * Returns the filename extension from a {@link String}, {@link File}, {@link URL} or
+     * {@link URI}. If no extension is found, returns an empty string.
+     *
+     * @param  path The path as a {@link String}, {@link File}, {@link URL} or {@link URI}.
+     * @return The filename extension in the given path, or an empty string if none.
+     */
+    public static String extension(final Object path) {
+        return IOUtilities.extension(path);
+    }
+
+    /**
+     * Write an empty FeatureCollection in a file
+     * @param f output file
+     * @throws IOException
+     */
+    public static void writeEmptyFeatureCollection(Path f) throws IOException {
+
+        try (OutputStream outStream = Files.newOutputStream(f, CREATE, WRITE, TRUNCATE_EXISTING);
+             JsonGenerator writer = GeoJSONParser.FACTORY.createGenerator(outStream, JsonEncoding.UTF8)) {
+
+            //start write feature collection.
+            writer.writeStartObject();
+            writer.writeStringField(TYPE, FEATURE_COLLECTION);
+            writer.writeArrayFieldStart(FEATURES);
+            writer.writeEndArray();
+            writer.writeEndObject();
+            writer.flush();
+        }
+    }
+
+    /**
+     * Useful method to help write an object into a JsonGenerator.
+     * This method can handle :
+     * <ul>
+     *     <li>Arrays</li>
+     *     <li>Collection</li>
+     *     <li>Numbers (Double, Float, Short, BigInteger, BigDecimal, integer, Long, Byte)</li>
+     *     <li>Boolean</li>
+     *     <li>String</li>
+     * </ul>
+     * @param value
+     * @param writer
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    public static void writeValue(Object value, JsonGenerator writer) throws IOException, IllegalArgumentException {
+
+        if (value == null) {
+            writer.writeNull();
+            return;
+        }
+
+        Class binding = value.getClass();
+
+        if (binding.isArray()) {
+            if (byte.class.isAssignableFrom(binding.getComponentType())) {
+                writer.writeBinary((byte[]) value);
+            } else {
+                writer.writeStartArray();
+                final int size = Array.getLength(value);
+                for (int i = 0; i < size; i++) {
+                    writeValue(Array.get(value, i), writer);
+                }
+                writer.writeEndArray();
+            }
+
+        } else if (Collection.class.isAssignableFrom(binding)) {
+            writer.writeStartArray();
+            Collection coll = (Collection) value;
+            for (Object obj : coll) {
+                writeValue(obj, writer);
+            }
+            writer.writeEndArray();
+
+        } else if (Double.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Double) value);
+        } else if (Float.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Float) value);
+        } else if (Short.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Short) value);
+        } else if (Byte.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Byte) value);
+        } else if (BigInteger.class.isAssignableFrom(binding)) {
+            writer.writeNumber((BigInteger) value);
+        } else if (BigDecimal.class.isAssignableFrom(binding)) {
+            writer.writeNumber((BigDecimal) value);
+        } else if (Integer.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Integer) value);
+        } else if (Long.class.isAssignableFrom(binding)) {
+            writer.writeNumber((Long) value);
+
+        } else if (Boolean.class.isAssignableFrom(binding)) {
+            writer.writeBoolean((Boolean) value);
+        } else if (String.class.isAssignableFrom(binding)) {
+            writer.writeString(String.valueOf(value));
+        } else {
+            //fallback
+            writer.writeString(String.valueOf(value));
+        }
+    }
+
+    /**
+     * Compare {@link JsonLocation} equality without sourceRef test.
+     * @param loc1
+     * @param loc2
+     * @return
+     */
+    public static boolean equals(JsonLocation loc1, JsonLocation loc2) {
+        if (loc1 == null) {
+            return (loc2 == null);
+        }
+
+        return loc2 != null && (loc1.getLineNr() == loc2.getLineNr() &&
+                loc1.getColumnNr() == loc2.getColumnNr() &&
+                loc1.getByteOffset() == loc2.getByteOffset() &&
+                loc1.getCharOffset() == loc2.getCharOffset());
+    }
+
+    /**
+     * Check wether the given data type contains an identifier property according
+     * to SIS convention (see {@link AttributeConvention#IDENTIFIER_PROPERTY}).
+     *
+     * @param toSearchIn The data type to scan for an identifier.
+     * @return True if an sis:identifier property is available. False otherwise.
+     */
+    public static boolean hasIdentifier(final FeatureType toSearchIn) {
+        try {
+            toSearchIn.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString());
+            return true;
+        } catch (PropertyNotFoundException ex) {
+            return false;
+        }
+    }
+
+    /**
+     * If an sis:identifier property is available (see
+     * {@link AttributeConvention#IDENTIFIER_PROPERTY}), we try to acquire it
+     * value type (see {@link AttributeType#getValueClass() }. If we cannot
+     * determine the value type for this property, we simply return an empty
+     * optional. Note that an error is thrown if the given feature type does not
+     * contain any identifier property.
+     *
+     * @param toSearchIn The property to extract identifier from.
+     * @return The value class of found property if we can determine it (i.e:
+     * it's an attribute or an operation from which we can unravel an
+     * attribute), or an empty object if the property cannot provide value
+     * class.
+     * @throws PropertyNotFoundException If no
+     * {@link AttributeConvention#IDENTIFIER_PROPERTY} is present in the input.
+     */
+    public static Optional<Class> getIdentifierType(final FeatureType toSearchIn) throws PropertyNotFoundException {
+        final PropertyType idProperty = toSearchIn.getProperty(AttributeConvention.IDENTIFIER_PROPERTY.toString());
+        return castOrUnwrap(idProperty).map(AttributeType::getValueClass);
+    }
+
+    /**
+     * Create a converter to set values of arbitrary type into the sis:identifier
+     * property of a given feature type.
+     * Note: RFC7946 specifies that identifier must be either numeric or string.
+     *
+     * @param target The feature type which specifies the sis:identifier, and by
+     * extension the output value class for the converter to create.
+     * @return A function capable of converting arbitrary objects into required
+     * type for sis:identifier property.
+     * @throws IllegalArgumentException If the given data type provides a bad value
+     * class for identifier property.
+     */
+    public static Function getIdentifierConverter(final FeatureType target) throws IllegalArgumentException {
+        final Class identifierType = GeoJSONUtils.getIdentifierType(target)
+                .orElseThrow(() -> new IllegalArgumentException("Cannot determine the value type for identifier property. Should either be a string or a number."));
+        final Function converter;
+        if (Numbers.isFloat(identifierType)) {
+            converter = input -> Double.parseDouble(input.toString());
+        } else if (Long.class.isAssignableFrom(identifierType)) {
+            converter = input -> Long.parseLong(input.toString());
+        } else if (Numbers.isInteger(identifierType)) {
+            converter = input -> Integer.parseInt(input.toString());
+        } else if (String.class.isAssignableFrom(identifierType)) {
+            converter = Object::toString;
+        } else {
+            throw new IllegalArgumentException("Unsupported type for identifier property. RFC 7946 asks for a string or number data.");
+        }
+
+        return input -> {
+            if (input == null || identifierType.getClass().isAssignableFrom(input.getClass())) {
+                return input;
+            }
+
+            return converter.apply(input);
+        };
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/LiteJsonLocation.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/LiteJsonLocation.java
new file mode 100644
index 0000000..3fe39f4
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/LiteJsonLocation.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson;
+
+import com.fasterxml.jackson.core.JsonLocation;
+
+import java.util.Objects;
+
+/**
+ * Lightweight pojo of {@link JsonLocation} without internal source object
+ * reference and offset. Because since 2.3.x+ of jackson byteOffset and
+ * charOffset values depend of underling source type. (InputStream to use
+ * byteOffset, BufferedReader to use charOffset)
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class LiteJsonLocation {
+
+    private final int lineNr;
+    private final int columnNr;
+
+    public LiteJsonLocation(JsonLocation location) {
+        this.lineNr = location.getLineNr();
+        this.columnNr = location.getColumnNr();
+    }
+
+    public int getLineNr() {
+        return lineNr;
+    }
+
+    public int getColumnNr() {
+        return columnNr;
+    }
+
+    /**
+     * Check if an JsonLocation position (line and column) is before current
+     * LiteJsonLocation.
+     *
+     * @param o JsonLocation
+     * @return true if before and false if input JsonLocation is equals or after
+     * current LiteJsonLocation
+     */
+    public boolean isBefore(JsonLocation o) {
+        if (o == null) {
+            return false;
+        }
+        LiteJsonLocation that = new LiteJsonLocation(o);
+
+        return lineNr < that.lineNr || (lineNr == that.lineNr && columnNr < that.columnNr);
+    }
+
+    /**
+     * Test equality with LiteJsonLocation and JsonLocation input objects
+     *
+     * @param o
+     * @return
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        // not equals if o is null or not an instance of LiteJsonLocation or JsonLocation
+        if (o == null
+                || (!LiteJsonLocation.class.isAssignableFrom(o.getClass())
+                && !JsonLocation.class.isAssignableFrom(o.getClass()))) {
+            return false;
+        }
+
+        LiteJsonLocation that;
+        if (JsonLocation.class.isAssignableFrom(o.getClass())) {
+            that = new LiteJsonLocation((JsonLocation) o);
+        } else {
+            that = (LiteJsonLocation) o;
+        }
+
+        return lineNr == that.lineNr && columnNr == that.columnNr;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(lineNr, columnNr);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("LiteJsonLocation{");
+        sb.append("lineNr=").append(lineNr);
+        sb.append(", columnNr=").append(columnNr);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONCRS.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONCRS.java
new file mode 100644
index 0000000..63401ef
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONCRS.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson.binding;
+
+import java.io.Serializable;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.sis.referencing.crs.AbstractCRS;
+import org.apache.sis.referencing.cs.AxesConvention;
+import static org.apache.sis.storage.geojson.GeoJSONConstants.*;
+import org.apache.sis.internal.geojson.GeoJSONUtils;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.FactoryException;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONCRS implements Serializable {
+
+    private String type;
+    private final Map<String, String> properties = new HashMap<>();
+
+    public GeoJSONCRS() {
+    }
+
+    public GeoJSONCRS(CoordinateReferenceSystem crs) {
+        type = CRS_NAME;
+        setCRS(crs);
+    }
+
+    public GeoJSONCRS(URL url, String crsType) {
+        type = CRS_LINK;
+        if (url != null && crsType != null) {
+            properties.put(HREF, url.toString());
+            properties.put(TYPE, crsType);
+        }
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    public CoordinateReferenceSystem getCRS() throws FactoryException, MalformedURLException {
+        if (type.equals(CRS_NAME)) {
+            String name = properties.get(NAME);
+            CoordinateReferenceSystem crs = org.apache.sis.referencing.CRS.forCode(name);
+            if (!name.startsWith("urn")) {
+                //legacy names, we force longitude first for those
+                crs = AbstractCRS.castOrCopy(crs).forConvention(AxesConvention.RIGHT_HANDED);
+            }
+            return crs;
+        } else if (type.equals(CRS_LINK)) {
+            final String href = properties.get(HREF);
+            final String crsType = properties.get(TYPE);
+            return GeoJSONUtils.parseCRS(href, crsType);
+        }
+        return null;
+    }
+
+    public void setCRS(CoordinateReferenceSystem crs) {
+        type = CRS_NAME;
+        GeoJSONUtils.toURN(crs)
+                .ifPresent(urn -> properties.put(NAME, urn));
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeature.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeature.java
new file mode 100644
index 0000000..a22a580
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeature.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson.binding;
+
+import org.apache.sis.storage.geojson.GeoJSONConstants;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONFeature extends GeoJSONObject {
+
+    private GeoJSONGeometry geometry;
+    /**
+     * Identifier (id attribute) of the feature. According to RFC 7946, it is
+     * optional and can either be a number or a string.
+     */
+    private Object id;
+    private Map<String, Object> properties = new HashMap<>();
+
+    public GeoJSONFeature() {
+        setType(GeoJSONConstants.FEATURE);
+    }
+
+    public GeoJSONGeometry getGeometry() {
+        return geometry;
+    }
+
+    public void setGeometry(GeoJSONGeometry geometry) {
+        this.geometry = geometry;
+    }
+
+    public Object getId() {
+        return id;
+    }
+
+    public void setId(Object id) {
+        this.id = id;
+    }
+
+    public Map<String, Object> getProperties() {
+        return properties;
+    }
+
+    public void setProperties(Map<String, Object> properties) {
+        this.properties = properties;
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeatureCollection.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeatureCollection.java
new file mode 100644
index 0000000..ba2465b
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONFeatureCollection.java
@@ -0,0 +1,196 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson.binding;
+
+import org.apache.sis.internal.geojson.GeoJSONParser;
+import org.apache.sis.internal.geojson.LiteJsonLocation;
+import org.apache.sis.storage.geojson.GeoJSONConstants;
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import org.apache.sis.util.collection.BackingStoreException;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONFeatureCollection extends GeoJSONObject implements Iterator<GeoJSONFeature>, Closeable {
+
+    private List<GeoJSONFeature> features = new ArrayList<>();
+
+    transient JsonLocation currentPos;
+    transient GeoJSONFeature current;
+    transient int currentIdx;
+    transient InputStream readStream;
+    transient JsonParser parser;
+
+    /**
+     * If current GeoJSONFeatureCollection is in lazy parsing mode, sourceInput
+     * should be not {@code null} and used to create {@link JsonParser object}
+     */
+    transient Path sourceInput;
+    transient LiteJsonLocation startPos;
+    transient LiteJsonLocation endPos;
+    transient Boolean lazyMode;
+
+    public GeoJSONFeatureCollection(Boolean lazyMode) {
+        setType(GeoJSONConstants.FEATURE_COLLECTION);
+        this.lazyMode = lazyMode;
+    }
+
+    public List<GeoJSONFeature> getFeatures() {
+        return features;
+    }
+
+    public void setFeatures(List<GeoJSONFeature> features) {
+        this.features = features;
+    }
+
+    public void setStartPosition(JsonLocation startPos) {
+        this.startPos = new LiteJsonLocation(startPos);
+    }
+
+    public void setEndPosition(JsonLocation endPos) {
+        this.endPos = new LiteJsonLocation(endPos);
+    }
+
+    public void setSourceInput(Path input) {
+        this.sourceInput = input;
+    }
+
+    public Boolean isLazyMode() {
+        return lazyMode;
+    }
+
+    @Override
+    public boolean hasNext() {
+        try {
+            findNext();
+            return current != null;
+        } catch (IOException e) {
+            throw new BackingStoreException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public GeoJSONFeature next() {
+        try {
+            findNext();
+            final GeoJSONFeature ob = current;
+            current = null;
+            if (ob == null) {
+                throw new BackingStoreException("No more feature.");
+            }
+            return ob;
+        } catch (IOException e) {
+            throw new BackingStoreException(e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Find next Feature from features list or as lazy parsing.
+     *
+     * @throws IOException
+     */
+    private void findNext() throws IOException {
+        if (current != null) {
+            return;
+        }
+        if (lazyMode) {
+            if (sourceInput == null || startPos == null || endPos == null) {
+                return;
+            }
+
+            if (parser == null) {
+                readStream = Files.newInputStream(sourceInput);
+                parser = GeoJSONParser.FACTORY.createParser(readStream);
+            }
+
+            //loop to FeatureCollection start
+            if (currentPos == null) {
+                while (!startPos.equals(currentPos)) {
+                    parser.nextToken();
+                    currentPos = parser.getCurrentLocation();
+                }
+            }
+
+            current = null;
+
+            // set parser to feature object start
+            while (parser.getCurrentToken() != JsonToken.START_OBJECT && !endPos.equals(currentPos)) {
+
+                if (parser.getCurrentToken() != JsonToken.START_OBJECT && endPos.isBefore(currentPos)) {
+                    //cannot find collection end token and no more start object token
+                    //break loop to avoid infinite search
+                    break;
+                }
+                parser.nextToken();
+                currentPos = parser.getCurrentLocation();
+            }
+
+            if (!endPos.equals(currentPos)) {
+                GeoJSONObject obj = GeoJSONParser.parseGeoJSONObject(parser);
+                if (obj instanceof GeoJSONFeature) {
+                    current = (GeoJSONFeature) obj;
+                }
+                currentPos = parser.getCurrentLocation();
+            }
+        } else {
+            if (currentIdx < features.size()) {
+                current = features.get(currentIdx++);
+            } else {
+                current = null;
+            }
+        }
+    }
+
+    @Override
+    public void remove() {
+        //do nothing
+    }
+
+    @Override
+    public void close() {
+        //close read stream
+        if (readStream != null) {
+            try {
+                readStream.close();
+            } catch (IOException e) {
+                throw new BackingStoreException(e.getMessage(), e);
+            }
+        }
+        //close parser
+        if (parser != null && !parser.isClosed()) {
+            try {
+                parser.close();
+            } catch (IOException e) {
+                throw new BackingStoreException(e.getMessage(), e);
+            }
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONGeometry.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONGeometry.java
new file mode 100644
index 0000000..1c86ea0
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONGeometry.java
@@ -0,0 +1,504 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson.binding;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.apache.sis.storage.geojson.GeoJSONConstants;
+import org.locationtech.jts.geom.Coordinate;
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryCollection;
+import org.locationtech.jts.geom.GeometryFactory;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.LinearRing;
+import org.locationtech.jts.geom.MultiLineString;
+import org.locationtech.jts.geom.MultiPoint;
+import org.locationtech.jts.geom.MultiPolygon;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.locationtech.jts.geom.impl.CoordinateArraySequence;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONGeometry extends GeoJSONObject implements Serializable {
+
+    public GeoJSONGeometry() {
+    }
+
+    /**
+     * POINT
+     */
+    public static class GeoJSONPoint extends GeoJSONGeometry {
+
+        private double[] coordinates;
+
+        public GeoJSONPoint() {
+            setType(GeoJSONConstants.POINT);
+        }
+
+        public double[] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[] coordinates) {
+            this.coordinates = coordinates;
+        }
+
+    }
+
+    /**
+     * MULTI-POINT
+     */
+    public static class GeoJSONMultiPoint extends GeoJSONGeometry {
+
+        private double[][] coordinates;
+
+        public GeoJSONMultiPoint() {
+            setType(GeoJSONConstants.MULTI_POINT);
+        }
+
+        public double[][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * LINESTRING
+     */
+    public static class GeoJSONLineString extends GeoJSONGeometry {
+
+        private double[][] coordinates;
+
+        public GeoJSONLineString() {
+            setType(GeoJSONConstants.LINESTRING);
+        }
+
+        public double[][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * MULTI-LINESTRING
+     */
+    public static class GeoJSONMultiLineString extends GeoJSONGeometry {
+
+        private double[][][] coordinates;
+
+        public GeoJSONMultiLineString() {
+            setType(GeoJSONConstants.MULTI_LINESTRING);
+        }
+
+        public double[][][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * POLYGON
+     */
+    public static class GeoJSONPolygon extends GeoJSONGeometry {
+
+        private double[][][] coordinates;
+
+        public GeoJSONPolygon() {
+            setType(GeoJSONConstants.POLYGON);
+        }
+
+        public double[][][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * MULTI-POLYGON
+     */
+    public static class GeoJSONMultiPolygon extends GeoJSONGeometry {
+
+        private double[][][][] coordinates;
+
+        public GeoJSONMultiPolygon() {
+            setType(GeoJSONConstants.MULTI_POLYGON);
+        }
+
+        public double[][][][] getCoordinates() {
+            return coordinates;
+        }
+
+        public void setCoordinates(double[][][][] coordinates) {
+            this.coordinates = coordinates;
+        }
+    }
+
+    /**
+     * GEOMETRY-COLLECTION
+     */
+    public static class GeoJSONGeometryCollection extends GeoJSONGeometry {
+
+        protected List<GeoJSONGeometry> geometries = new ArrayList<GeoJSONGeometry>();
+
+        public GeoJSONGeometryCollection() {
+            setType(GeoJSONConstants.GEOMETRY_COLLECTION);
+        }
+
+        public List<GeoJSONGeometry> getGeometries() {
+            return geometries;
+        }
+
+        public void setGeometries(List<GeoJSONGeometry> geometries) {
+            this.geometries = geometries;
+        }
+    }
+
+    private static final GeometryFactory GF = new GeometryFactory();
+
+    /**
+     * Convert GeoJSONGeometry into JTS Geometry with included CRS
+     *
+     * @param jsonGeometry
+     * @param crs
+     * @return JTS Geometry
+     */
+    public static Geometry toJTS(GeoJSONGeometry jsonGeometry, CoordinateReferenceSystem crs) {
+
+        if (jsonGeometry != null) {
+            if (crs == null) {
+                throw new IllegalArgumentException("Null Coordinate Reference System.");
+            }
+
+            if (jsonGeometry instanceof GeoJSONPoint) {
+                return toJTS((GeoJSONPoint) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONLineString) {
+                return toJTS((GeoJSONLineString) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONPolygon) {
+                return toJTS((GeoJSONPolygon) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONMultiPoint) {
+                return toJTS((GeoJSONMultiPoint) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONMultiLineString) {
+                return toJTS((GeoJSONMultiLineString) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONMultiPolygon) {
+                return toJTS((GeoJSONMultiPolygon) jsonGeometry, crs);
+            } else if (jsonGeometry instanceof GeoJSONGeometryCollection) {
+                return toJTS((GeoJSONGeometryCollection) jsonGeometry, crs);
+            } else {
+                throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry);
+            }
+        }
+        return null;
+    }
+
+    private static Coordinate toCoordinate(double[] coord) {
+        if (coord.length == 2) {
+            return new Coordinate(coord[0], coord[1]);
+        } else if (coord.length == 3) {
+            return new Coordinate(coord[0], coord[1], coord[2]);
+        } else {
+            throw new IllegalArgumentException("Coordinates not valid : " + Arrays.toString(coord));
+        }
+    }
+
+    private static CoordinateSequence toCoordinateSequence(double[][] coords) {
+
+        Coordinate[] coordinates = new Coordinate[coords.length];
+        if (coords.length > 0) {
+            for (int i = 0; i < coords.length; i++) {
+                coordinates[i] = toCoordinate(coords[i]);
+            }
+        }
+        return new CoordinateArraySequence(coordinates);
+    }
+
+    private static LinearRing toLinearRing(double[][] coords) {
+        return GF.createLinearRing(toCoordinateSequence(coords));
+    }
+
+    private static Polygon toPolygon(double[][][] coords, CoordinateReferenceSystem crs) {
+
+        LinearRing exterior = toLinearRing(coords[0]);
+        LinearRing[] holes = new LinearRing[coords.length - 1];
+        if (coords.length > 1) {
+            for (int i = 0; i < holes.length; i++) {
+                holes[i] = toLinearRing(coords[i + 1]);
+            }
+        }
+
+        Polygon polygon = GF.createPolygon(exterior, holes);
+        polygon.setUserData(crs);
+        return polygon;
+    }
+
+    private static Point toJTS(GeoJSONPoint jsonPoint, CoordinateReferenceSystem crs) {
+        double[] coord = jsonPoint.getCoordinates();
+
+        final Point pt = GF.createPoint(toCoordinate(coord));
+        pt.setUserData(crs);
+        return pt;
+    }
+
+    private static LineString toJTS(GeoJSONLineString jsonLS, CoordinateReferenceSystem crs) {
+        double[][] coord = jsonLS.getCoordinates();
+
+        LineString line = GF.createLineString(toCoordinateSequence(coord));
+        line.setUserData(crs);
+        return line;
+    }
+
+    private static Geometry toJTS(GeoJSONPolygon jsonPolygon, CoordinateReferenceSystem crs) {
+        double[][][] coord = jsonPolygon.getCoordinates();
+
+        if (coord.length <= 0) {
+            return GF.buildGeometry(Collections.EMPTY_LIST);
+        }
+
+        return toPolygon(coord, crs);
+    }
+
+    private static MultiPoint toJTS(GeoJSONMultiPoint jsonMP, CoordinateReferenceSystem crs) {
+        double[][] coords = jsonMP.getCoordinates();
+
+        Coordinate[] coordinates = new Coordinate[coords.length];
+        if (coords.length > 0) {
+            for (int i = 0; i < coords.length; i++) {
+                coordinates[i] = toCoordinate(coords[i]);
+            }
+        }
+
+        MultiPoint mpt = GF.createMultiPoint(GF.getCoordinateSequenceFactory().create(coordinates));
+        mpt.setUserData(crs);
+        return mpt;
+    }
+
+    private static MultiLineString toJTS(GeoJSONMultiLineString jsonMLS, CoordinateReferenceSystem crs) {
+        double[][][] coords = jsonMLS.getCoordinates();
+
+        LineString[] lines = new LineString[coords.length];
+        if (coords.length > 0) {
+            for (int i = 0; i < coords.length; i++) {
+                lines[i] = GF.createLineString(toCoordinateSequence(coords[i]));
+            }
+        }
+
+        MultiLineString mls = GF.createMultiLineString(lines);
+        mls.setUserData(crs);
+        return mls;
+    }
+
+    private static MultiPolygon toJTS(GeoJSONMultiPolygon jsonMP, CoordinateReferenceSystem crs) {
+        double[][][][] coords = jsonMP.getCoordinates();
+
+        Polygon[] polygons = new Polygon[coords.length];
+        if (coords.length > 0) {
+            for (int i = 0; i < coords.length; i++) {
+                polygons[i] = toPolygon(coords[i], crs);
+            }
+        }
+
+        MultiPolygon mp = GF.createMultiPolygon(polygons);
+        mp.setUserData(crs);
+        return mp;
+    }
+
+    private static GeometryCollection toJTS(GeoJSONGeometryCollection jsonGC, CoordinateReferenceSystem crs) {
+        if (jsonGC.getGeometries() != null) {
+
+            int size = jsonGC.getGeometries().size();
+            Geometry[] geometries = new Geometry[size];
+
+            for (int i = 0; i < size; i++) {
+                geometries[i] = toJTS(jsonGC.getGeometries().get(i), crs);
+            }
+
+            GeometryCollection gc = GF.createGeometryCollection(geometries);
+            gc.setUserData(crs);
+            return gc;
+        }
+        return null;
+    }
+
+    /**
+     * Convert JTS geometry into a GeoJSONGeometry.
+     *
+     * @param geom JTS Geometry
+     * @return GeoJSONGeometry
+     */
+    public static GeoJSONGeometry toGeoJSONGeometry(Geometry geom) {
+        if (geom == null) {
+            throw new IllegalArgumentException("Null Geometry.");
+        }
+
+        if (geom instanceof Point) {
+            return toGeoJSONGeometry((Point) geom);
+        } else if (geom instanceof LineString) {
+            return toGeoJSONGeometry((LineString) geom);
+        } else if (geom instanceof Polygon) {
+            return toGeoJSONGeometry((Polygon) geom);
+        } else if (geom instanceof MultiPoint) {
+            return toGeoJSONGeometry((MultiPoint) geom);
+        } else if (geom instanceof MultiLineString) {
+            return toGeoJSONGeometry((MultiLineString) geom);
+        } else if (geom instanceof MultiPolygon) {
+            return toGeoJSONGeometry((MultiPolygon) geom);
+        } else if (geom instanceof GeometryCollection) {
+            return toGeoJSONGeometry((GeometryCollection) geom);
+        } else {
+            throw new IllegalArgumentException("Unsupported geometry type : " + geom);
+        }
+    }
+
+    private static double[] toArray(Coordinate coord) {
+        double x = coord.getOrdinate(0);
+        double y = coord.getOrdinate(1);
+        //do not use getOrdinate for Z, may raise an exception
+        double z = coord.getZ();
+
+        if (Double.isNaN(z)) {
+            return new double[]{x, y};
+        } else {
+            return new double[]{x, y, z};
+        }
+    }
+
+    private static double[][] toArray(Coordinate[] coords) {
+        double[][] result = new double[coords.length][];
+
+        for (int i = 0; i < coords.length; i++) {
+            result[i] = toArray(coords[i]);
+        }
+        return result;
+    }
+
+    private static double[][] toArray(CoordinateSequence coords) {
+        return toArray(coords.toCoordinateArray());
+    }
+
+    private static double[][][] toArray(CoordinateSequence[] coords) {
+        double[][][] result = new double[coords.length][][];
+
+        for (int i = 0; i < coords.length; i++) {
+            result[i] = toArray(coords[i]);
+        }
+        return result;
+    }
+
+    private static double[][][][] toArray(CoordinateSequence[][] coords) {
+        double[][][][] result = new double[coords.length][][][];
+
+        for (int i = 0; i < coords.length; i++) {
+            result[i] = toArray(coords[i]);
+        }
+        return result;
+    }
+
+    private static GeoJSONPoint toGeoJSONGeometry(Point pt) {
+        GeoJSONPoint jsonPt = new GeoJSONPoint();
+        jsonPt.setCoordinates(toArray(pt.getCoordinate()));
+        return jsonPt;
+    }
+
+    private static GeoJSONLineString toGeoJSONGeometry(LineString line) {
+        GeoJSONLineString jsonln = new GeoJSONLineString();
+        jsonln.setCoordinates(toArray(line.getCoordinateSequence()));
+        return jsonln;
+    }
+
+    private static GeoJSONPolygon toGeoJSONGeometry(Polygon polygon) {
+        GeoJSONPolygon jsonpoly = new GeoJSONPolygon();
+        CoordinateSequence[] coords = getCoordinateSequencesFromPolygon(polygon);
+        jsonpoly.setCoordinates(toArray(coords));
+        return jsonpoly;
+    }
+
+    private static CoordinateSequence[] getCoordinateSequencesFromPolygon(Polygon polygon) {
+        int totalRings = polygon.getNumInteriorRing() + 1;
+        CoordinateSequence[] coords = new CoordinateSequence[totalRings];
+        coords[0] = polygon.getExteriorRing().getCoordinateSequence();
+
+        if (totalRings > 1) {
+            for (int i = 0; i < totalRings - 1; i++) {
+                coords[i + 1] = polygon.getInteriorRingN(i).getCoordinateSequence();
+            }
+        }
+        return coords;
+    }
+
+    private static GeoJSONMultiPoint toGeoJSONGeometry(MultiPoint mpt) {
+        GeoJSONMultiPoint jsonMpt = new GeoJSONMultiPoint();
+        jsonMpt.setCoordinates(toArray(mpt.getCoordinates()));
+        return jsonMpt;
+    }
+
+    private static GeoJSONMultiLineString toGeoJSONGeometry(MultiLineString mln) {
+        GeoJSONMultiLineString jsonMln = new GeoJSONMultiLineString();
+        int totalRings = mln.getNumGeometries();
+        CoordinateSequence[] coords = new CoordinateSequence[totalRings];
+        for (int i = 0; i < totalRings; i++) {
+            coords[i] = ((LineString) mln.getGeometryN(i)).getCoordinateSequence();
+        }
+        jsonMln.setCoordinates(toArray(coords));
+        return jsonMln;
+    }
+
+    private static GeoJSONMultiPolygon toGeoJSONGeometry(MultiPolygon multiPolygon) {
+        GeoJSONMultiPolygon jsonMPoly = new GeoJSONMultiPolygon();
+        int totalPoly = multiPolygon.getNumGeometries();
+
+        CoordinateSequence[][] coords = new CoordinateSequence[totalPoly][];
+        for (int i = 0; i < totalPoly; i++) {
+            coords[i] = getCoordinateSequencesFromPolygon((Polygon) multiPolygon.getGeometryN(i));
+        }
+
+        jsonMPoly.setCoordinates(toArray(coords));
+        return jsonMPoly;
+    }
+
+    private static GeoJSONGeometryCollection toGeoJSONGeometry(GeometryCollection geometryCollection) {
+        GeoJSONGeometryCollection coll = new GeoJSONGeometryCollection();
+        int numGeometries = geometryCollection.getNumGeometries();
+
+        for (int i = 0; i < numGeometries; i++) {
+            coll.getGeometries().add(toGeoJSONGeometry(geometryCollection.getGeometryN(i)));
+        }
+
+        return coll;
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONObject.java b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONObject.java
new file mode 100644
index 0000000..44ead79
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/internal/geojson/binding/GeoJSONObject.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.geojson.binding;
+
+import com.fasterxml.jackson.annotation.JsonSubTypes;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+import static org.apache.sis.internal.geojson.binding.GeoJSONGeometry.*;
+
+import java.io.Serializable;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
+@JsonSubTypes({
+    @JsonSubTypes.Type(value = GeoJSONFeatureCollection.class, name = "FeatureCollection"),
+    @JsonSubTypes.Type(value = GeoJSONFeature.class, name = "Feature"),
+    @JsonSubTypes.Type(value = GeoJSONPoint.class, name = "Point"),
+    @JsonSubTypes.Type(value = GeoJSONLineString.class, name = "LineString"),
+    @JsonSubTypes.Type(value = GeoJSONPolygon.class, name = "Polygon"),
+    @JsonSubTypes.Type(value = GeoJSONMultiPoint.class, name = "MultiPoint"),
+    @JsonSubTypes.Type(value = GeoJSONMultiLineString.class, name = "MultiLineString"),
+    @JsonSubTypes.Type(value = GeoJSONMultiPolygon.class, name = "MultiPolygon"),
+    @JsonSubTypes.Type(value = GeoJSONGeometryCollection.class, name = "GeometryCollection")
+})
+public class GeoJSONObject implements Serializable {
+
+    private String type;
+    private double[] bbox;
+    private GeoJSONCRS crs;
+
+    public GeoJSONObject() {
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public double[] getBbox() {
+        return bbox;
+    }
+
+    public void setBbox(double[] bbox) {
+        this.bbox = bbox;
+    }
+
+    public GeoJSONCRS getCrs() {
+        return crs;
+    }
+
+    public void setCrs(GeoJSONCRS crs) {
+        this.crs = crs;
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java
new file mode 100644
index 0000000..4d1c94e
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.java
@@ -0,0 +1,212 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.storage.geojson;
+
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+import org.apache.sis.util.iso.ResourceInternationalString;
+import org.apache.sis.util.resources.IndexedResourceBundle;
+import org.opengis.util.InternationalString;
+
+
+/**
+ * Locale-dependent resources for words or simple sentences.
+ */
+public final class Bundle extends IndexedResourceBundle {
+    /**
+     * Resource keys. This class is used when compiling sources, but no dependencies to
+     * {@code Keys} should appear in any resulting class files. Since the Java compiler
+     * inlines final integer values, using long identifiers will not bloat the constant
+     * pools of compiled classes.
+     */
+    public static final class Keys {
+        private Keys() {
+        }
+
+        /**
+         * Number of decimals
+         */
+        public static final short coordinate_accuracy = 1;
+
+        /**
+         * Number of decimals (default 7).
+         */
+        public static final short coordinate_accuracy_remarks = 2;
+
+        /**
+         * GeoJSON data file (.json)
+         */
+        public static final short datastoreDescription = 3;
+
+        public static final short datastoreFolderDescription = 4;
+
+        public static final short datastoreFolderTitle = 5;
+
+        /**
+         * GeoJSON
+         */
+        public static final short datastoreTitle = 6;
+    }
+
+    /**
+     * Constructs a new resource bundle loading data from the given UTF file.
+     *
+     * @param filename The file or the JAR entry containing resources.
+     */
+    public Bundle(final java.net.URL filename) {
+        super(filename);
+    }
+
+    /**
+     * Returns resources in the given locale.
+     *
+     * @param  locale The locale, or {@code null} for the default locale.
+     * @return Resources in the given locale.
+     * @throws MissingResourceException if resources can't be found.
+     */
+    public static Bundle getResources(Locale locale) throws MissingResourceException {
+        return getBundle(Bundle.class, locale);
+    }
+
+    /**
+     * The international string to be returned by {@link formatInternational}.
+     */
+    private static final class International extends ResourceInternationalString {
+        private static final long serialVersionUID = -9199238559657784488L;
+
+        International(final int key) {
+            super(Bundle.class.getName(), String.valueOf(key));
+        }
+
+        @Override
+        protected ResourceBundle getBundle(final Locale locale) {
+            return getResources(locale);
+        }
+    }
+
+    /**
+     * Gets an international string for the given key. This method does not check for the key
+     * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown
+     * when a {@link InternationalString#toString} method is invoked.
+     *
+     * @param  key The key for the desired string.
+     * @return An international string for the given key.
+     */
+    public static InternationalString formatInternational(final short key) {
+        return new International(key);
+    }
+
+    /**
+     * Gets an international string for the given key. This method does not check for the key
+     * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown
+     * when a {@link InternationalString#toString} method is invoked.
+     *
+     * {@note This method is redundant with the one expecting <code>Object...</code>, but is
+     *        provided for binary compatibility with previous Geotk versions. It also avoid the
+     *        creation of a temporary array. There is no risk of confusion since the two methods
+     *        delegate their work to the same <code>format</code> method anyway.}
+     *
+     * @param  key The key for the desired string.
+     * @param  arg Values to substitute to "{0}".
+     * @return An international string for the given key.
+     *
+     * @todo Current implementation just invokes {@link #format}. Need to format only when
+     *       {@code toString(Locale)} is invoked.
+     */
+    public static InternationalString formatInternational(final short key, final Object arg) {
+        return new org.apache.sis.util.iso.SimpleInternationalString(format(key, arg));
+    }
+
+    /**
+     * Gets an international string for the given key. This method does not check for the key
+     * validity. If the key is invalid, then a {@link MissingResourceException} may be thrown
+     * when a {@link InternationalString#toString} method is invoked.
+     *
+     * @param  key The key for the desired string.
+     * @param  args Values to substitute to "{0}", "{1}", <i>etc</i>.
+     * @return An international string for the given key.
+     *
+     * @todo Current implementation just invokes {@link #format}. Need to format only when
+     *       {@code toString(Locale)} is invoked.
+     */
+    public static InternationalString formatInternational(final short key, final Object... args) {
+        return new org.apache.sis.util.iso.SimpleInternationalString(format(key, args));
+    }
+
+    /**
+     * Gets a string for the given key from this resource bundle or one of its parents.
+     *
+     * @param  key The key for the desired string.
+     * @return The string for the given key.
+     * @throws MissingResourceException If no object for the given key can be found.
+     */
+    public static String format(final short key) throws MissingResourceException {
+        return getResources(null).getString(key);
+    }
+
+    /**
+     * Gets a string for the given key are replace all occurrence of "{0}"
+     * with values of {@code arg0}.
+     *
+     * @param  key The key for the desired string.
+     * @param  arg0 Value to substitute to "{0}".
+     * @return The formatted string for the given key.
+     * @throws MissingResourceException If no object for the given key can be found.
+     */
+    public static String format(final short  key,
+                                final Object arg0) throws MissingResourceException
+    {
+        return getResources(null).getString(key, arg0);
+    }
+
+    /**
+     * Gets a string for the given key are replace all occurrence of "{0}",
+     * "{1}", with values of {@code arg0}, {@code arg1}.
+     *
+     * @param  key The key for the desired string.
+     * @param  arg0 Value to substitute to "{0}".
+     * @param  arg1 Value to substitute to "{1}".
+     * @return The formatted string for the given key.
+     * @throws MissingResourceException If no object for the given key can be found.
+     */
+    public static String format(final short  key,
+                                final Object arg0,
+                                final Object arg1) throws MissingResourceException
+    {
+        return getResources(null).getString(key, arg0, arg1);
+    }
+
+    /**
+     * Gets a string for the given key are replace all occurrence of "{0}",
+     * "{1}", with values of {@code arg0}, {@code arg1}, etc.
+     *
+     * @param  key The key for the desired string.
+     * @param  arg0 Value to substitute to "{0}".
+     * @param  arg1 Value to substitute to "{1}".
+     * @param  arg2 Value to substitute to "{2}".
+     * @return The formatted string for the given key.
+     * @throws MissingResourceException If no object for the given key can be found.
+     */
+    public static String format(final short  key,
+                                final Object arg0,
+                                final Object arg1,
+                                final Object arg2) throws MissingResourceException
+    {
+        return getResources(null).getString(key, arg0, arg1, arg2);
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties
new file mode 100644
index 0000000..819dbb2
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle.properties
@@ -0,0 +1,5 @@
+
+datastoreTitle=GeoJSON
+datastoreDescription=GeoJSON data file (.json)
+coordinate_accuracy=Number of decimals
+coordinate_accuracy_remarks=Number of decimals (default 7).
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties
new file mode 100644
index 0000000..819dbb2
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_en.properties
@@ -0,0 +1,5 @@
+
+datastoreTitle=GeoJSON
+datastoreDescription=GeoJSON data file (.json)
+coordinate_accuracy=Number of decimals
+coordinate_accuracy_remarks=Number of decimals (default 7).
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties
new file mode 100644
index 0000000..9938477
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/Bundle_fr.properties
@@ -0,0 +1,5 @@
+
+datastoreTitle=GeoJSON
+datastoreDescription=fichier GeoJSON de donn\u00e9es vectorielles (.json)
+coordinate_accuracy=Chiffres apr\u00e8s la virgule
+coordinate_accuracy_remarks=Nombre de chiffres apr\u00e8s la virgule.
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONConstants.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONConstants.java
new file mode 100644
index 0000000..fbe2780
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONConstants.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.storage.geojson;
+
+import org.apache.sis.util.Static;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONConstants extends Static {
+
+    public static final String FEATURE_COLLECTION = "FeatureCollection";
+    public static final String FEATURE = "Feature";
+
+    public static final String POINT = "Point";
+    public static final String LINESTRING = "LineString";
+    public static final String POLYGON = "Polygon";
+    public static final String MULTI_POINT = "MultiPoint";
+    public static final String MULTI_LINESTRING = "MultiLineString";
+    public static final String MULTI_POLYGON = "MultiPolygon";
+    public static final String GEOMETRY_COLLECTION = "GeometryCollection";
+
+    public static final String CRS_NAME = "name";
+    public static final String CRS_LINK = "link";
+
+    public static final String CRS_TYPE_PROJ4 = "proj4";
+    public static final String CRS_TYPE_OGCWKT = "ogcwkt";
+    public static final String CRS_TYPE_ESRIWKT = "esriwkt";
+
+    public static final String TYPE = "type";
+    public static final String FEATURES = "features";
+    public static final String GEOMETRY = "geometry";
+    public static final String GEOMETRIES = "geometries";
+    public static final String COORDINATES = "coordinates";
+    public static final String PROPERTIES = "properties";
+    public static final String CRS = "crs";
+    public static final String NAME = "name";
+    public static final String HREF = "href";
+    public static final String BBOX = "bbox";
+    public static final String ID = "id";
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java
new file mode 100644
index 0000000..8aa4c0f
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONFileWriter.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.locks.ReadWriteLock;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+final class GeoJSONFileWriter extends GeoJSONReader {
+
+    private final GeoJSONWriter writer;
+
+    private Feature edited;
+    private Feature lastWritten;
+    private Path tmpFile;
+
+    public GeoJSONFileWriter(Path jsonFile, FeatureType featureType, ReadWriteLock rwLock,
+            final String encoding, final int doubleAccuracy) throws DataStoreException {
+        super(jsonFile, featureType, rwLock);
+
+        JsonEncoding jsonEncoding = JsonEncoding.UTF8;
+
+        try {
+            final String name = featureType.getName().tip().toString();
+            tmpFile = jsonFile.resolveSibling(name + ".wjson");
+            writer = new GeoJSONWriter(tmpFile, jsonEncoding, doubleAccuracy, false);
+
+            //start write feature collection.
+            writer.writeStartFeatureCollection(crs, null);
+            writer.flush();
+        } catch (IOException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
+    }
+
+    @Override
+    public FeatureType getFeatureType() {
+        return super.getFeatureType();
+    }
+
+    @Override
+    public Feature next() throws BackingStoreException {
+        try {
+            write();
+            edited = super.next();
+        } catch (BackingStoreException ex) {
+            //we reach append mode
+            //create empty feature
+            edited = featureType.newInstance();
+            if (hasIdentifier) {
+                edited.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(currentFeatureIdx++));
+            }
+        }
+        return edited;
+    }
+
+    public void write(Feature edited) throws BackingStoreException {
+        this.edited = edited;
+        write();
+    }
+
+    public void write() throws BackingStoreException {
+        if (edited == null || edited.equals(lastWritten)) {
+            return;
+        }
+
+        lastWritten = edited;
+        try {
+            writer.writeFeature(edited);
+            writer.flush();
+        } catch (IOException | IllegalArgumentException e) {
+            throw new BackingStoreException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void remove() {
+        edited = null;
+    }
+
+    @Override
+    public void close() {
+        try (GeoJSONWriter toClose = writer) {
+            toClose.writeEndFeatureCollection();
+            toClose.flush();
+        } catch (IOException ex) {
+            throw new BackingStoreException(ex);
+        } finally {
+            super.close();
+        }
+
+        //flip files
+        rwlock.writeLock().lock();
+        try {
+            Files.move(tmpFile, jsonFile, StandardCopyOption.REPLACE_EXISTING);
+        } catch (IOException ex) {
+            throw new BackingStoreException(ex);
+        } finally {
+            rwlock.writeLock().unlock();
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java
new file mode 100644
index 0000000..3781740
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONProvider.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.storage.geojson;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.net.URI;
+import java.nio.ByteBuffer;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.sis.internal.storage.Capability;
+import org.apache.sis.internal.storage.StoreMetadata;
+import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreProvider;
+import static org.apache.sis.storage.DataStoreProvider.LOCATION;
+import org.apache.sis.storage.FeatureSet;
+import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.storage.StorageConnector;
+import org.opengis.parameter.ParameterDescriptor;
+import org.opengis.parameter.ParameterDescriptorGroup;
+import org.opengis.parameter.ParameterValueGroup;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+@StoreMetadata(
+        formatName = GeoJSONProvider.NAME,
+        capabilities = {Capability.READ, Capability.WRITE, Capability.CREATE},
+        fileSuffixes = {"json", "geojson", "topojson"},
+        resourceTypes = {FeatureSet.class})
+public final class GeoJSONProvider extends DataStoreProvider {
+
+    public static final String NAME = "geojson";
+
+    public static final String ENCODING = "UTF-8";
+
+    /**
+     * The {@value} MIME type.
+     */
+    public static final String MIME_TYPE = "application/json";
+
+    private static final List<String> EXTENSIONS = Arrays.asList("json", "geojson", "topojson");
+
+    public static final ParameterDescriptor<URI> PATH = new ParameterBuilder()
+            .addName(LOCATION)
+            .addName("path")
+            .setRequired(true)
+            .create(URI.class, null);
+
+    /**
+     * Optional
+     */
+    public static final ParameterDescriptor<Integer> COORDINATE_ACCURACY = new ParameterBuilder()
+            .addName("coordinate_accuracy")
+            .addName(Bundle.formatInternational(Bundle.Keys.coordinate_accuracy))
+            .setRemarks(Bundle.formatInternational(Bundle.Keys.coordinate_accuracy_remarks))
+            .setRequired(false)
+            .create(Integer.class, 7);
+
+    public static final ParameterDescriptorGroup PARAMETERS_DESCRIPTOR =
+            new ParameterBuilder()
+                    .addName(NAME)
+                    .addName(Bundle.formatInternational(Bundle.Keys.datastoreTitle))
+                    .setDescription(Bundle.formatInternational(Bundle.Keys.datastoreDescription))
+                    .createGroup(PATH, COORDINATE_ACCURACY);
+
+    @Override
+    public String getShortName() {
+        return NAME;
+    }
+
+    /**
+     * {@inheritDoc }
+     */
+    @Override
+    public ParameterDescriptorGroup getOpenParameters() {
+        return PARAMETERS_DESCRIPTOR;
+    }
+
+    /**
+     * {@inheritDoc }
+     */
+    @Override
+    public GeoJSONStore open(final ParameterValueGroup params) throws DataStoreException {
+        return new GeoJSONStore(this, params);
+    }
+
+    @Override
+    public ProbeResult probeContent(StorageConnector connector) throws DataStoreException {
+        Path p = connector.getStorageAs(Path.class);
+        String extension = IOUtilities.extension(p).toLowerCase();
+        if (EXTENSIONS.contains(extension)) {
+            try {
+                final ByteBuffer buffer = connector.getStorageAs(ByteBuffer.class);
+                final Reader reader;
+                if (buffer != null) {
+                    buffer.mark();
+                    reader = null;
+                } else {
+                    // User gave us explicitely a Reader (e.g. a StringReader wrapping a String instance).
+                    reader = connector.getStorageAs(Reader.class);
+                    if (reader == null) {
+                        return ProbeResult.UNSUPPORTED_STORAGE;
+                    }
+                    reader.mark(2048); // Should be no more than {@code StorageConnector.DEFAULT_BUFFER_SIZE / 2}
+                }
+                boolean ok = false;
+                if (nextAfterSpaces(buffer, reader) == '{') {
+                    ok = true;
+                }
+                if (buffer != null) {
+                    buffer.reset();
+                } else {
+                    reader.reset();
+                }
+                if (ok) {
+                    return new ProbeResult(true, MIME_TYPE, null);
+                }
+            } catch (IOException e) {
+                throw new DataStoreException(e);
+            }
+        }
+        return ProbeResult.UNSUPPORTED_STORAGE;
+
+    }
+
+    /**
+     * Returns the next character which is not a white space, or -1 if the end
+     * of stream is reached. Exactly one of {@code buffer} and {@code reader}
+     * shall be non-null.
+     */
+    private static int nextAfterSpaces(final ByteBuffer buffer, final Reader reader) throws IOException {
+        if (buffer != null) {
+            while (buffer.hasRemaining()) {
+                final char c = (char) buffer.get();
+                if (!Character.isWhitespace(c)) {
+                    return c;
+                }
+            }
+            return -1;
+        }
+        int c;
+        while ((c = IOUtilities.readCodePoint(reader)) >= 0) {
+            if (!Character.isWhitespace(c)) {
+                break;
+            }
+        }
+        return c;
+    }
+
+    @Override
+    public DataStore open(StorageConnector connector) throws DataStoreException {
+        try {
+            final Path path = connector.getStorageAs(Path.class);
+            return new GeoJSONStore(this, path, null);
+        } catch (IllegalArgumentException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
+    }
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java
new file mode 100644
index 0000000..bfb0ef5
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONReader.java
@@ -0,0 +1,356 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.storage.geojson;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.nio.file.Path;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.internal.geojson.binding.GeoJSONFeature;
+import org.apache.sis.internal.geojson.binding.GeoJSONFeatureCollection;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry;
+import org.apache.sis.internal.geojson.binding.GeoJSONObject;
+import org.apache.sis.internal.geojson.GeoJSONParser;
+import org.apache.sis.internal.geojson.GeoJSONUtils;
+import org.apache.sis.util.ObjectConverter;
+import org.apache.sis.util.ObjectConverters;
+import org.apache.sis.util.UnconvertibleObjectException;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.apache.sis.util.logging.Logging;
+import org.locationtech.jts.geom.Geometry;
+import org.opengis.feature.Attribute;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+class GeoJSONReader implements Iterator<Feature>, AutoCloseable {
+
+    private static final Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson");
+    private final Map<Map.Entry<Class, Class>, ObjectConverter> convertersCache = new HashMap<>();
+
+    private GeoJSONObject jsonObj;
+    private Boolean toRead = true;
+
+    protected final ReadWriteLock rwlock;
+    protected final FeatureType featureType;
+    protected final Path jsonFile;
+    protected Feature current;
+    protected int currentFeatureIdx;
+
+    /**
+     * A flag indicating if we should read identifiers from read stream. it's
+     * activated if the feature type given at built contains an
+     * {@link AttributeConvention#IDENTIFIER_PROPERTY}.
+     */
+    protected final boolean hasIdentifier;
+    final Function idConverter;
+    final CoordinateReferenceSystem crs;
+    final String geometryName;
+
+    public GeoJSONReader(Path jsonFile, FeatureType featureType, ReadWriteLock rwLock) {
+        hasIdentifier = GeoJSONUtils.hasIdentifier(featureType);
+        if (hasIdentifier) {
+            idConverter = GeoJSONUtils.getIdentifierConverter(featureType);
+        } else {
+            // It should not be used, but we don't set it to null in case someons use it by mistake.
+            idConverter = input -> input;
+        }
+
+        final PropertyType defaultGeometry = GeoJSONUtils.getDefaultGeometry(featureType);
+        crs = GeoJSONUtils.getCRS(defaultGeometry);
+        geometryName = defaultGeometry.getName().toString();
+
+        this.jsonFile = jsonFile;
+        this.featureType = featureType;
+        this.rwlock = rwLock;
+    }
+
+    public FeatureType getFeatureType() {
+        return featureType;
+    }
+
+    @Override
+    public boolean hasNext() throws BackingStoreException {
+        read();
+        return current != null;
+    }
+
+    @Override
+    public Feature next() throws BackingStoreException {
+        read();
+        final Feature ob = current;
+        current = null;
+        if (ob == null) {
+            throw new BackingStoreException("No more records.");
+        }
+        return ob;
+    }
+
+    private void read() throws BackingStoreException {
+        if (current != null) return;
+
+        //first call
+        if (toRead) {
+            rwlock.readLock().lock();
+            try {
+                jsonObj = GeoJSONParser.parse(jsonFile, true);
+            } catch (IOException e) {
+                throw new BackingStoreException(e);
+            } finally {
+                toRead = false;
+                rwlock.readLock().unlock();
+            }
+        }
+
+        if (jsonObj instanceof GeoJSONFeatureCollection) {
+            final GeoJSONFeatureCollection fc = (GeoJSONFeatureCollection) jsonObj;
+            rwlock.readLock().lock();
+            try {
+                if (fc.hasNext()) {
+                    current = toFeature(fc.next());
+                    currentFeatureIdx++;
+                }
+            } finally {
+                rwlock.readLock().unlock();
+            }
+        } else if (jsonObj instanceof GeoJSONFeature) {
+            current = toFeature((GeoJSONFeature) jsonObj);
+            jsonObj = null;
+        } else if (jsonObj instanceof GeoJSONGeometry) {
+            current = toFeature((GeoJSONGeometry) jsonObj);
+            jsonObj = null;
+        }
+    }
+
+    /**
+     * Convert a GeoJSONFeature to geotk Feature.
+     *
+     * @param jsonFeature
+     * @param featureId
+     * @return
+     */
+    protected Feature toFeature(GeoJSONFeature jsonFeature) throws BackingStoreException {
+
+        //Build geometry
+        final Geometry geom = GeoJSONGeometry.toJTS(jsonFeature.getGeometry(), crs);
+
+        //empty feature
+        final Feature feature = featureType.newInstance();
+        if (hasIdentifier) {
+            Object id = jsonFeature.getId();
+            if (id == null) {
+                id = currentFeatureIdx;
+            }
+            feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(id));
+        }
+        feature.setPropertyValue(geometryName, geom);
+
+        //recursively fill other properties
+        final Map<String, Object> properties = jsonFeature.getProperties();
+        fillFeature(feature, properties);
+
+        return feature;
+    }
+
+    /**
+     * Recursively fill a ComplexAttribute with properties map
+     *
+     * @param feature
+     * @param properties
+     */
+    private void fillFeature(Feature feature, Map<String, Object> properties) throws BackingStoreException {
+        final FeatureType featureType = feature.getType();
+
+        for (final PropertyType type : featureType.getProperties(true)) {
+
+            final String attName = type.getName().toString();
+            final Object value = properties.get(attName);
+            if (value == null) {
+                continue;
+            }
+
+            if (type instanceof FeatureAssociationRole) {
+                final FeatureAssociationRole asso = (FeatureAssociationRole) type;
+                final FeatureType assoType = asso.getValueType();
+                final Class<?> valueClass = value.getClass();
+
+                if (valueClass.isArray()) {
+                    Class<?> base = value.getClass().getComponentType();
+
+                    if (!Map.class.isAssignableFrom(base)) {
+                        LOGGER.log(Level.WARNING, "Invalid complex property value " + value);
+                    }
+
+                    final int size = Array.getLength(value);
+                    if (size > 0) {
+                        //list of objects
+                        final List<Feature> subs = new ArrayList<>();
+                        for (int i = 0; i < size; i++) {
+                            final Feature subComplexAttribute = assoType.newInstance();
+                            fillFeature(subComplexAttribute, (Map) Array.get(value, i));
+                            subs.add(subComplexAttribute);
+                        }
+                        feature.setPropertyValue(attName, subs);
+                    }
+                } else if (value instanceof Map) {
+                    final Feature subComplexAttribute = assoType.newInstance();
+                    fillFeature(subComplexAttribute, (Map) value);
+                    feature.setPropertyValue(attName, subComplexAttribute);
+                }
+
+            } else if (type instanceof AttributeType) {
+                final Attribute<?> property = (Attribute<?>) feature.getProperty(type.getName().toString());
+                fillProperty(property, value);
+            }
+        }
+    }
+
+    /**
+     * Try to convert value as expected in PropertyType description.
+     *
+     * @param prop
+     * @param value
+     */
+    private void fillProperty(Attribute prop, Object value) throws BackingStoreException {
+
+        Object convertValue = null;
+        try {
+            if (value != null) {
+                final AttributeType<?> propertyType = prop.getType();
+                final Class binding = propertyType.getValueClass();
+
+                if (value.getClass().isArray() && binding.isArray()) {
+
+                    int nbdim = 1;
+                    Class base = value.getClass().getComponentType();
+                    while (base.isArray()) {
+                        base = base.getComponentType();
+                        nbdim++;
+                    }
+
+                    convertValue = rebuildArray(value, base, nbdim);
+
+                } else {
+                    convertValue = convert(value, binding);
+                }
+            }
+        } catch (UnconvertibleObjectException e1) {
+            throw new BackingStoreException(String.format("Inconvertible property %s : %s",
+                    prop.getName().tip().toString(), e1.getMessage()), e1);
+        }
+
+        prop.setValue(convertValue);
+    }
+
+    /**
+     * Rebuild nDim arrays recursively
+     *
+     * @param candidate
+     * @param componentType
+     * @param depth
+     * @return Array object
+     * @throws UnconvertibleObjectException
+     */
+    private Object rebuildArray(Object candidate, Class componentType, int depth) throws UnconvertibleObjectException {
+        if (candidate == null) {
+            return null;
+        }
+
+        if (candidate.getClass().isArray()) {
+            final int size = Array.getLength(candidate);
+            final int[] dims = new int[depth];
+            dims[0] = size;
+            final Object rarray = Array.newInstance(componentType, dims);
+            depth--;
+            for (int k = 0; k < size; k++) {
+                Array.set(rarray, k, rebuildArray(Array.get(candidate, k), componentType, depth));
+            }
+            return rarray;
+        } else {
+            return convert(candidate, componentType);
+        }
+    }
+
+    /**
+     * Convert value object into binding class
+     *
+     * @param value
+     * @param binding
+     * @return
+     * @throws UnconvertibleObjectException
+     */
+    private Object convert(Object value, Class binding) throws UnconvertibleObjectException {
+        AbstractMap.SimpleEntry<Class, Class> key = new AbstractMap.SimpleEntry<>(value.getClass(), binding);
+        ObjectConverter converter = convertersCache.get(key);
+
+        if (converter == null) {
+            converter = ObjectConverters.find(value.getClass(), binding);
+            convertersCache.put(key, converter);
+        }
+        return converter.apply(value);
+    }
+
+    /**
+     * Convert a GeoJSONGeometry to Feature.
+     *
+     * @param jsonGeometry
+     * @return
+     */
+    protected Feature toFeature(GeoJSONGeometry jsonGeometry) {
+        final Feature feature = featureType.newInstance();
+        final Geometry geom = GeoJSONGeometry.toJTS(jsonGeometry, crs);
+        feature.setPropertyValue(geometryName, geom);
+        return feature;
+    }
+
+    @Override
+    public void remove() {
+        throw new BackingStoreException("Not supported on reader.");
+    }
+
+    @Override
+    public void close() {
+        try {
+            // If our object is a feature collection, it could get an opened connexion to a file. We must dispose it.
+            if (jsonObj instanceof AutoCloseable) {
+                ((AutoCloseable) jsonObj).close();
+            }
+        } catch (Exception e) {
+            LOGGER.log(Level.WARNING, "Cannot close a read resource.", e);
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java
new file mode 100644
index 0000000..b8a5ed7
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStore.java
@@ -0,0 +1,459 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.storage.geojson;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.*;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Predicate;
+import java.util.function.UnaryOperator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.AttributeTypeBuilder;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.geojson.FeatureTypeUtils;
+import org.apache.sis.internal.geojson.GeoJSONParser;
+import org.apache.sis.internal.geojson.GeoJSONUtils;
+import org.apache.sis.internal.geojson.binding.GeoJSONFeature;
+import org.apache.sis.internal.geojson.binding.GeoJSONFeatureCollection;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import org.apache.sis.internal.geojson.binding.GeoJSONObject;
+import org.apache.sis.internal.storage.ResourceOnFileSystem;
+import org.apache.sis.metadata.iso.DefaultMetadata;
+import org.apache.sis.parameter.Parameters;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreProvider;
+import org.apache.sis.storage.WritableFeatureSet;
+import static org.apache.sis.storage.geojson.GeoJSONProvider.*;
+import org.apache.sis.util.iso.Names;
+import org.apache.sis.util.logging.Logging;
+import org.locationtech.jts.geom.*;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.geometry.Envelope;
+import org.opengis.metadata.Metadata;
+import org.opengis.parameter.ParameterValueGroup;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.util.GenericName;
+
+/**
+ *
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONStore extends DataStore implements ResourceOnFileSystem, WritableFeatureSet {
+
+    private static final Logger LOGGER = Logging.getLogger("org.apache.sis.storage.geojson");
+    private static final String DESC_FILE_SUFFIX = "_Type.json";
+
+    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
+    private final DataStoreProvider provider;
+    private final ParameterValueGroup parameters;
+
+    private GenericName name;
+    private FeatureType featureType;
+    private Path descFile;
+    private Path jsonFile;
+    private Integer coordAccuracy;
+    private boolean isLocal = true;
+
+    public GeoJSONStore(DataStoreProvider provider, final Path path, Integer coordAccuracy)
+            throws DataStoreException {
+        this(provider, toParameter(path.toUri(), coordAccuracy));
+    }
+
+    public GeoJSONStore(DataStoreProvider provider, final URI uri, Integer coordAccuracy)
+            throws DataStoreException {
+        this(provider, toParameter(uri, coordAccuracy));
+    }
+
+    public GeoJSONStore(DataStoreProvider provider, final ParameterValueGroup params) throws DataStoreException {
+        super();
+        this.provider = provider;
+        this.parameters = params;
+        this.coordAccuracy = (Integer) params.parameter(COORDINATE_ACCURACY.getName().toString()).getValue();
+
+        final URI uri = (URI) params.parameter(PATH.getName().toString()).getValue();
+
+        //FIXME
+        this.isLocal = "file".equalsIgnoreCase(uri.getScheme());
+
+        Path tmpFile = null;
+        try {
+            tmpFile = Paths.get(uri);
+        } catch (FileSystemNotFoundException ex) {
+            throw new DataStoreException(ex);
+        }
+
+        final String fileName = tmpFile.getFileName().toString();
+        if (fileName.endsWith(DESC_FILE_SUFFIX)) {
+            this.descFile = tmpFile;
+            this.jsonFile = descFile.resolveSibling(fileName.replace(DESC_FILE_SUFFIX, ".json"));
+        } else {
+            this.jsonFile = tmpFile;
+            //search for description json file
+            String typeName = GeoJSONUtils.getNameWithoutExt(jsonFile);
+            this.descFile = jsonFile.resolveSibling(typeName + DESC_FILE_SUFFIX);
+        }
+    }
+
+    private static ParameterValueGroup toParameter(final URI uri, Integer coordAccuracy) {
+        final Parameters params = Parameters.castOrWrap(GeoJSONProvider.PARAMETERS_DESCRIPTOR.createValue());
+        params.getOrCreate(GeoJSONProvider.PATH).setValue(uri);
+        params.getOrCreate(GeoJSONProvider.COORDINATE_ACCURACY).setValue(coordAccuracy);
+        return params;
+    }
+
+    @Override
+    public Optional<ParameterValueGroup> getOpenParameters() {
+        return Optional.of(parameters);
+    }
+
+    @Override
+    public Metadata getMetadata() throws DataStoreException {
+        return new DefaultMetadata();
+    }
+
+    @Override
+    public DataStoreProvider getProvider() {
+        return provider;
+    }
+
+    @Override
+    public Optional<GenericName> getIdentifier() throws DataStoreException {
+        checkTypeExist();
+        return Optional.of(name);
+    }
+
+    @Override
+    public FeatureType getType() throws DataStoreException {
+        checkTypeExist();
+        return featureType;
+    }
+
+    public boolean isWritable() throws DataStoreException {
+        return isLocal && Files.isWritable(descFile) && Files.isWritable(jsonFile);
+    }
+
+    private void checkTypeExist() throws DataStoreException {
+        if (name == null || featureType == null) {
+            try {
+                // try to parse file only if exist and not empty
+                if (Files.exists(jsonFile) && Files.size(jsonFile) != 0) {
+                    featureType = readType();
+                    name = featureType.getName();
+                }
+            } catch (IOException e) {
+                LOGGER.log(Level.WARNING, e.getMessage(), e);
+            }
+        }
+    }
+
+    /**
+     * Read FeatureType from a JSON-Schema file if exist or directly from the
+     * input JSON file.
+     *
+     * @return
+     * @throws DataStoreException
+     * @throws IOException
+     */
+    private FeatureType readType() throws DataStoreException, IOException {
+        if (Files.exists(descFile) && Files.size(descFile) != 0) {
+            // build FeatureType from description JSON.
+            return FeatureTypeUtils.readFeatureType(descFile);
+        } else {
+            if (Files.exists(jsonFile) && Files.size(jsonFile) != 0) {
+                final String name = GeoJSONUtils.getNameWithoutExt(jsonFile);
+
+                final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+                ftb.setName(name);
+
+                // build FeatureType from the first Feature of JSON file.
+                final GeoJSONObject obj = GeoJSONParser.parse(jsonFile, true);
+                if (obj == null) {
+                    throw new DataStoreException("Invalid GeoJSON file " + jsonFile.toString());
+                }
+
+                CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(obj);
+
+                if (obj instanceof GeoJSONFeatureCollection) {
+                    GeoJSONFeatureCollection jsonFeatureCollection = (GeoJSONFeatureCollection) obj;
+                    if (!jsonFeatureCollection.hasNext()) {
+                        //empty FeatureCollection error ?
+                        throw new DataStoreException("Empty GeoJSON FeatureCollection " + jsonFile.toString());
+                    } else {
+
+                        // TODO should we analyse all Features from FeatureCollection to be sure
+                        // that each Feature properties JSON object define exactly the same properties
+                        // with the same bindings ?
+                        GeoJSONFeature jsonFeature = jsonFeatureCollection.next();
+                        fillTypeFromFeature(ftb, crs, jsonFeature, false);
+                    }
+
+                } else if (obj instanceof GeoJSONFeature) {
+                    GeoJSONFeature jsonFeature = (GeoJSONFeature) obj;
+                    fillTypeFromFeature(ftb, crs, jsonFeature, true);
+                } else if (obj instanceof GeoJSONGeometry) {
+                    ftb.addAttribute(String.class).setName("fid").addRole(AttributeRole.IDENTIFIER_COMPONENT);
+                    ftb.addAttribute(findBinding((GeoJSONGeometry) obj)).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY);
+                }
+
+                return ftb.build();
+            } else {
+                throw new DataStoreException("Can't create FeatureType from empty/not found Json file " + jsonFile.getFileName().toString());
+            }
+        }
+    }
+
+    private void fillTypeFromFeature(FeatureTypeBuilder ftb, CoordinateReferenceSystem crs,
+            GeoJSONFeature jsonFeature, boolean analyseGeometry) {
+        if (analyseGeometry) {
+            ftb.addAttribute(findBinding(jsonFeature.getGeometry())).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        } else {
+            ftb.addAttribute(Geometry.class).setName("geometry").setCRS(crs).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        }
+        for (Map.Entry<String, Object> property : jsonFeature.getProperties().entrySet()) {
+            final Object value = property.getValue();
+            final Class<?> binding = value != null ? value.getClass() : String.class;
+            final GenericName name = Names.createLocalName(null, null, property.getKey());
+            final AttributeTypeBuilder<?> atb = ftb.addAttribute(binding).setName(name);
+            if ("id".equals(property.getKey()) || "fid".equals(property.getKey())) {
+                atb.addRole(AttributeRole.IDENTIFIER_COMPONENT);
+            }
+        }
+    }
+
+    private Class<? extends Geometry> findBinding(GeoJSONGeometry jsonGeometry) {
+
+        if (jsonGeometry instanceof GeoJSONPoint) {
+            return Point.class;
+        } else if (jsonGeometry instanceof GeoJSONLineString) {
+            return LineString.class;
+        } else if (jsonGeometry instanceof GeoJSONPolygon) {
+            return Polygon.class;
+        } else if (jsonGeometry instanceof GeoJSONMultiPoint) {
+            return MultiPoint.class;
+        } else if (jsonGeometry instanceof GeoJSONMultiLineString) {
+            return MultiLineString.class;
+        } else if (jsonGeometry instanceof GeoJSONMultiPolygon) {
+            return MultiPolygon.class;
+        } else if (jsonGeometry instanceof GeoJSONGeometryCollection) {
+            return GeometryCollection.class;
+        } else {
+            throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry);
+        }
+
+    }
+
+    private void writeType(FeatureType featureType) throws DataStoreException {
+        try {
+            final boolean jsonExist = Files.exists(jsonFile);
+
+            if (jsonExist && Files.size(jsonFile) != 0) {
+                throw new DataStoreException(String.format("Non empty json file %s can't create new json file %s",
+                        jsonFile.getFileName().toString(), featureType.getName()));
+            }
+
+            if (!jsonExist) {
+                Files.createFile(jsonFile);
+            }
+            //create json with empty collection
+            GeoJSONUtils.writeEmptyFeatureCollection(jsonFile);
+
+            //json schema file
+            final boolean descExist = Files.exists(descFile);
+
+            if (descExist && Files.size(descFile) != 0) {
+                throw new DataStoreException(String.format("Non empty json schema file %s can't create new json schema %s",
+                        descFile.getFileName().toString(), featureType.getName()));
+            }
+
+            if (!descExist) {
+                Files.createFile(descFile);
+            }
+            //create json schema file
+            FeatureTypeUtils.writeFeatureType(featureType, descFile);
+
+            this.featureType = featureType;
+            this.name = featureType.getName();
+        } catch (IOException e) {
+            throw new DataStoreException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public Optional<Envelope> getEnvelope() throws DataStoreException {
+        Envelope envelope = null;
+        rwLock.readLock().lock();
+        try {
+            final GeoJSONObject obj = GeoJSONParser.parse(jsonFile, true);
+            final CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(obj);
+            envelope = GeoJSONUtils.getEnvelope(obj, crs);
+        } catch (IOException e) {
+            throw new DataStoreException(e.getMessage(), e);
+        } finally {
+            rwLock.readLock().unlock();
+        }
+
+        return Optional.ofNullable(envelope);
+    }
+
+    /**
+     * {@inheritDoc }
+     */
+    @Override
+    public Stream<Feature> features(boolean parallel) throws DataStoreException {
+        final GeoJSONReader reader = new GeoJSONReader(jsonFile, featureType, rwLock);
+        final Stream<Feature> stream = StreamSupport.stream(Spliterators.spliteratorUnknownSize(reader, Spliterator.ORDERED), false);
+        stream.onClose(reader::close);
+        return stream;
+    }
+
+    @Override
+    public void add(Iterator<? extends Feature> features) throws DataStoreException {
+        try (GeoJSONFileWriter writer = getFeatureWriter()) {
+            //rewrite existing features
+            while (writer.hasNext()) {
+                writer.next();
+            }
+            //new features
+            while (features.hasNext()) {
+                Feature feature = features.next();
+                Feature next = writer.next();
+                writer.write(feature);
+            }
+        }
+    }
+
+    @Override
+    public boolean removeIf(Predicate<? super Feature> filter) throws DataStoreException {
+        boolean modified = false;
+        try (GeoJSONFileWriter writer = getFeatureWriter()) {
+            //rewrite existing features
+            while (writer.hasNext()) {
+                Feature feature = writer.next();
+                if (filter.test(feature)) {
+                    writer.remove();
+                    modified = true;
+                }
+            }
+        }
+        return modified;
+    }
+
+    @Override
+    public void replaceIf(Predicate<? super Feature> filter, UnaryOperator<Feature> updater) throws DataStoreException {
+        try (GeoJSONFileWriter writer = getFeatureWriter()) {
+            //rewrite existing features
+            while (writer.hasNext()) {
+                Feature feature = writer.next();
+                if (filter.test(feature)) {
+                    Feature changed = updater.apply(feature);
+                    if (changed == null) {
+                        writer.remove();
+                    } else {
+                        writer.write();
+                    }
+                }
+            }
+        }
+    }
+
+    private GeoJSONFileWriter getFeatureWriter() throws DataStoreException {
+        return new GeoJSONFileWriter(jsonFile, featureType, rwLock,
+                GeoJSONProvider.ENCODING, coordAccuracy);
+    }
+
+    @Override
+    public void updateType(final FeatureType featureType) throws DataStoreException {
+        if (!isLocal) {
+            throw new DataStoreException("Cannot create FeatureType on remote GeoJSON");
+        }
+        GenericName typeName = featureType.getName();
+        if (typeName == null) {
+            throw new DataStoreException("Type name can not be null.");
+        }
+        if (!typeName.tip().toString().equals(GeoJSONUtils.getNameWithoutExt(jsonFile))) {
+            throw new DataStoreException("New type name should be equals to file name.");
+        }
+
+        //delete previous files
+        rwLock.writeLock().lock();
+        try {
+            Files.deleteIfExists(descFile);
+            Files.deleteIfExists(jsonFile);
+            Files.createFile(jsonFile);
+        } catch (IOException e) {
+            throw new DataStoreException("Can not delete GeoJSON schema.", e);
+        } finally {
+            rwLock.writeLock().unlock();
+        }
+
+        //create new type
+        rwLock.writeLock().lock();
+        try {
+            writeType(featureType);
+        } finally {
+            rwLock.writeLock().unlock();
+        }
+    }
+
+    public void clearCache() {
+        name = null;
+        featureType = null;
+    }
+
+    @Override
+    public void close() throws DataStoreException {
+    }
+
+    /**
+     * {@inheritDoc }
+     */
+    @Override
+    public Path[] getComponentFiles() throws DataStoreException {
+        List<Path> files = new ArrayList<>();
+        if (Files.exists(jsonFile)) {
+            files.add(jsonFile);
+        }
+        if (Files.exists(descFile)) {
+            files.add(descFile);
+        }
+        return files.toArray(new Path[files.size()]);
+    }
+
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java
new file mode 100644
index 0000000..35c483c
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONStreamWriter.java
@@ -0,0 +1,220 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.feature.builder.PropertyTypeBuilder;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.FeatureNaming;
+import org.apache.sis.storage.IllegalNameException;
+import org.apache.sis.internal.geojson.GeoJSONUtils;
+import org.apache.sis.util.collection.BackingStoreException;
+import org.locationtech.jts.geom.Geometry;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.Operation;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public final class GeoJSONStreamWriter implements Iterator<Feature>, AutoCloseable {
+
+    private final GeoJSONWriter writer;
+    private final FeatureType featureType;
+
+    private Feature edited;
+    private Feature lastWritten;
+    private int currentFeatureIdx;
+
+    private final boolean hasIdentifier;
+
+    final Function idConverter;
+
+    /**
+     *
+     * @param outputStream stream were GeoJSON will be written
+     * @param featureType {@link FeatureType} of features to write.
+     * @param doubleAccuracy number of coordinates fraction digits
+     * @throws DataStoreException
+     */
+    public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final int doubleAccuracy)
+            throws DataStoreException {
+        this(outputStream, featureType, JsonEncoding.UTF8, doubleAccuracy);
+    }
+
+    /**
+     *
+     * @param outputStream stream were GeoJSON will be written
+     * @param featureType {@link FeatureType} of features to write.
+     * @param encoding character encoding
+     * @param doubleAccuracy number of coordinates fraction digits
+     * @throws DataStoreException
+     */
+    public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final JsonEncoding encoding, final int doubleAccuracy)
+            throws DataStoreException {
+        this(outputStream, featureType, encoding, doubleAccuracy, false);
+    }
+
+    public GeoJSONStreamWriter(OutputStream outputStream, FeatureType featureType, final JsonEncoding encoding, final int doubleAccuracy, boolean prettyPrint)
+            throws DataStoreException {
+
+        //remove any operation attribute
+        List<Operation> geometries = featureType.getProperties(true).stream()
+                .filter(Operation.class::isInstance)
+                .map(Operation.class::cast)
+                .filter(AttributeConvention::isGeometryAttribute)
+                .filter(GeoJSONUtils.IS_NOT_CONVENTION)
+                .collect(Collectors.toList());
+
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder(featureType);
+        final Iterator<PropertyTypeBuilder> it = ftb.properties().iterator();
+        final FeatureNaming naming = new FeatureNaming();
+        geometries.stream()
+                .map(Operation::getName)
+                .forEach(name -> {
+                    try {
+                        naming.add(null, name, name);
+                    } catch (IllegalNameException e) {
+                        //hack
+                    }
+                });
+        while (it.hasNext()) {
+            try {
+                naming.get(null, it.next().getName().toString());
+                it.remove();
+            } catch (IllegalNameException e) {
+                // normal behavior
+            }
+        }
+
+        for (final Operation op : geometries) {
+            GeoJSONUtils.castOrUnwrap(op).ifPresent(ftb::addAttribute);
+        }
+
+        this.featureType = ftb.build();
+        hasIdentifier = GeoJSONUtils.hasIdentifier(featureType);
+        if (hasIdentifier) {
+            idConverter = GeoJSONUtils.getIdentifierConverter(featureType);
+        } else {
+            // It should not be used, but we don't set it to null in case someons use it by mistake.
+            idConverter = input -> input;
+        }
+
+        try {
+            writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint);
+            //start write feature collection.
+            writer.writeStartFeatureCollection(GeoJSONUtils.getCRS(featureType), null);
+            writer.flush();
+        } catch (IOException ex) {
+            throw new DataStoreException(ex.getMessage(), ex);
+        }
+    }
+
+    /**
+     * Utility method to write a single Feature into an OutputStream
+     *
+     * @param outputStream
+     * @param feature to write
+     * @param encoding
+     * @param doubleAccuracy
+     * @param prettyPrint
+     */
+    public static void writeSingleFeature(OutputStream outputStream, Feature feature, final JsonEncoding encoding,
+            final int doubleAccuracy, boolean prettyPrint) throws IOException {
+
+        try (GeoJSONWriter writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint)) {
+            writer.writeSingleFeature(feature);
+        }
+    }
+
+    /**
+     * Utility method to write a single Geometry into an OutputStream
+     *
+     * @param outputStream
+     * @param geometry to write
+     * @param encoding
+     * @param doubleAccuracy
+     * @param prettyPrint
+     */
+    public static void writeSingleGeometry(OutputStream outputStream, Geometry geometry, final JsonEncoding encoding,
+            final int doubleAccuracy, boolean prettyPrint) throws IOException {
+
+        try (GeoJSONWriter writer = new GeoJSONWriter(outputStream, encoding, doubleAccuracy, prettyPrint)) {
+            writer.writeSingleGeometry(geometry);
+        }
+    }
+
+    public FeatureType getFeatureType() {
+        return featureType;
+    }
+
+    @Override
+    public Feature next() throws BackingStoreException {
+        edited = featureType.newInstance();
+        if (hasIdentifier) {
+            edited.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), idConverter.apply(currentFeatureIdx++));
+        }
+        return edited;
+    }
+
+    @Override
+    public void remove() throws BackingStoreException {
+        throw new BackingStoreException("Not supported on reader.");
+    }
+
+    public void write() throws BackingStoreException {
+        if (edited == null || edited.equals(lastWritten)) {
+            return;
+        }
+
+        lastWritten = edited;
+        try {
+            writer.writeFeature(edited);
+            writer.flush();
+        } catch (IOException | IllegalArgumentException e) {
+            throw new BackingStoreException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public boolean hasNext() throws BackingStoreException {
+        return true;
+    }
+
+    @Override
+    public void close() {
+        try {
+            writer.writeEndFeatureCollection();
+            writer.flush();
+            writer.close();
+        } catch (IOException ex) {
+            throw new BackingStoreException(ex);
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java
new file mode 100644
index 0000000..c919d12
--- /dev/null
+++ b/storage/sis-geojson/src/main/java/org/apache/sis/storage/geojson/GeoJSONWriter.java
@@ -0,0 +1,469 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonEncoding;
+import com.fasterxml.jackson.core.JsonGenerator;
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+import java.text.NumberFormat;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONGeometryCollection;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONLineString;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiLineString;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPoint;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONMultiPolygon;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPoint;
+import org.apache.sis.internal.geojson.binding.GeoJSONGeometry.GeoJSONPolygon;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.internal.geojson.GeoJSONParser;
+import static org.apache.sis.storage.geojson.GeoJSONConstants.*;
+import org.apache.sis.internal.geojson.GeoJSONUtils;
+import org.apache.sis.util.Utilities;
+import org.locationtech.jts.geom.Geometry;
+import org.opengis.feature.Attribute;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.Operation;
+import org.opengis.feature.PropertyNotFoundException;
+import org.opengis.feature.PropertyType;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+final class GeoJSONWriter implements Closeable, Flushable {
+
+    private static final String SYS_LF;
+
+    static {
+        String lf = null;
+        try {
+            lf = System.getProperty("line.separator");
+        } catch (Throwable t) {
+            // access exception?
+        }
+        SYS_LF = (lf == null) ? "\n" : lf;
+    }
+
+    private final JsonGenerator writer;
+    private final OutputStream outputStream;
+    private boolean first = true;
+    private boolean prettyPrint = true;
+
+    // state boolean to ensure that we can't call writeStartFeatureCollection
+    // if we first called writeSingleFeature
+    private boolean isFeatureCollection;
+    private boolean isSingleFeature;
+    private boolean isSingleGeometry;
+
+    private final NumberFormat numberFormat;
+
+    GeoJSONWriter(Path file, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException {
+        this(Files.newOutputStream(file, CREATE, WRITE, TRUNCATE_EXISTING), encoding, doubleAccuracy, prettyPrint);
+    }
+
+    GeoJSONWriter(OutputStream stream, JsonEncoding encoding, int doubleAccuracy, boolean prettyPrint) throws IOException {
+        this.prettyPrint = prettyPrint;
+        this.outputStream = null;
+        if (prettyPrint) {
+            this.writer = GeoJSONParser.FACTORY.createGenerator(stream, encoding).useDefaultPrettyPrinter();
+        } else {
+            this.writer = GeoJSONParser.FACTORY.createGenerator(stream, encoding);
+        }
+
+        this.writer.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, true);
+        numberFormat = NumberFormat.getInstance(Locale.US);
+        numberFormat.setGroupingUsed(false);
+        numberFormat.setMaximumFractionDigits(doubleAccuracy);
+    }
+
+    void writeStartFeatureCollection(CoordinateReferenceSystem crs, Envelope envelope) throws IOException {
+
+        assert (!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "Can't write FeatureCollection if we start a single feature or geometry GeoJSON.";
+        isFeatureCollection = true;
+
+        writer.writeStartObject();
+        writeNewLine();
+
+        writer.writeStringField(TYPE, FEATURE_COLLECTION);
+        writeNewLine();
+
+        if (crs != null && !Utilities.equalsApproximately(crs, CommonCRS.defaultGeographic())) {
+            if (writeCRS(crs)) {
+                writeNewLine();
+            } else {
+                throw new IOException("Cannot determine a valid URN for " + crs.getName());
+            }
+        }
+
+        if (envelope != null) {
+            //TODO write bbox
+            writeNewLine();
+        }
+    }
+
+    void writeEndFeatureCollection() throws IOException {
+        assert (isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "Can't write FeatureCollection end before writeStartFeatureCollection().";
+
+        if (!first) {
+            writer.writeEndArray(); //close feature collection array
+        }
+        writer.writeEndObject(); //close root object
+    }
+
+    /**
+     * Write GeoJSON with a single feature
+     *
+     * @param feature
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    void writeSingleFeature(Feature feature) throws IOException, IllegalArgumentException {
+        assert (!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "writeSingleFeature can called only once per GeoJSONWriter.";
+
+        isSingleFeature = true;
+        writeFeature(feature, true);
+    }
+
+    void writeFeature(Feature feature) throws IOException, IllegalArgumentException {
+        assert (isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "Can't write a Feature before writeStartFeatureCollection.";
+        writeFeature(feature, false);
+    }
+
+    /**
+     * Write a Feature.
+     *
+     * @param feature
+     * @param single
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    private void writeFeature(Feature feature, boolean single) throws IOException, IllegalArgumentException {
+        if (!single) {
+            if (first) {
+                writer.writeArrayFieldStart(FEATURES);
+                writeNewLine();
+                first = false;
+            }
+        }
+
+        writer.writeStartObject();
+        writer.writeStringField(TYPE, FEATURE);
+        /* As defined in GeoJSON spec, identifier is an optional attribute. For
+         * more details, see https://tools.ietf.org/html/rfc7946#section-3.2
+         */
+        try {
+            final Object idValue = feature.getPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString());
+            // TODO : search for a property named id or identifier ?
+            if (idValue != null) {
+                writeAttribute(ID, idValue, true);
+            }
+        } catch (PropertyNotFoundException e) {
+            GeoJSONParser.LOGGER.log(Level.FINE, "Cannot write ID cause no matching property has been found.", e);
+        }
+
+        //write CRS
+        if (single) {
+            final CoordinateReferenceSystem crs = GeoJSONUtils.getCRS(feature.getType());
+            if (crs != null && !Utilities.equalsApproximately(crs, CommonCRS.defaultGeographic())) {
+                if (!writeCRS(crs)) {
+                    throw new IOException("Cannot determine a valid URN for " + crs.getName());
+                }
+            }
+        }
+
+        //write geometry
+        final Optional<Geometry> geom = GeoJSONUtils.getDefaultGeometryValue(feature)
+                .filter(Geometry.class::isInstance)
+                .map(Geometry.class::cast);
+
+        if (geom.isPresent()) {
+            writer.writeFieldName(GEOMETRY);
+            writeFeatureGeometry(geom.get());
+        }
+
+        //write properties
+        writeProperties(feature, PROPERTIES, true);
+        writer.writeEndObject();
+
+        if (!single && !prettyPrint) {
+            writer.writeRaw(SYS_LF);
+        }
+    }
+
+    private void writeNewLine() throws IOException {
+        if (!prettyPrint) {
+            writer.writeRaw(SYS_LF);
+        }
+    }
+
+    /**
+     * Write CoordinateReferenceSystem
+     *
+     * @param crs
+     * @throws IOException
+     */
+    private boolean writeCRS(CoordinateReferenceSystem crs) throws IOException {
+        final Optional<String> urn = GeoJSONUtils.toURN(crs);
+        if (urn.isPresent()) {
+            writer.writeObjectFieldStart(CRS);
+            writer.writeStringField(TYPE, CRS_NAME);
+            writer.writeObjectFieldStart(PROPERTIES);
+            writer.writeStringField(NAME, urn.get());
+            writer.writeEndObject();//close properties
+            writer.writeEndObject();//close crs
+        }
+
+        return urn.isPresent();
+    }
+
+    /**
+     * Write ComplexAttribute.
+     *
+     * @param edited
+     * @param fieldName
+     * @param writeFieldName
+     * @throws IOException
+     * @throws IllegalArgumentException
+     */
+    private void writeProperties(Feature edited, String fieldName, boolean writeFieldName)
+            throws IOException, IllegalArgumentException {
+        if (writeFieldName) {
+            writer.writeObjectFieldStart(fieldName);
+        } else {
+            writer.writeStartObject();
+        }
+
+        FeatureType type = edited.getType();
+        Collection<? extends PropertyType> descriptors = type.getProperties(true).stream()
+                .filter(GeoJSONUtils.IS_NOT_CONVENTION)
+                .collect(Collectors.toList());
+        for (PropertyType propType : descriptors) {
+            if (AttributeConvention.contains(propType.getName())) continue;
+            if (AttributeConvention.isGeometryAttribute(propType)) continue;
+            final String name = propType.getName().tip().toString();
+            final Object value = edited.getPropertyValue(propType.getName().toString());
+
+            if (propType instanceof AttributeType) {
+                final AttributeType attType = (AttributeType) propType;
+                if (attType.getMaximumOccurs() > 1) {
+                    writer.writeArrayFieldStart(name);
+                    for (Object v : (Collection) value) {
+                        writeProperty(name, v, false);
+                    }
+                    writer.writeEndArray();
+                } else {
+                    writeProperty(name, value, true);
+                }
+            } else if (propType instanceof FeatureAssociationRole) {
+                final FeatureAssociationRole asso = (FeatureAssociationRole) propType;
+                if (asso.getMaximumOccurs() > 1) {
+                    writer.writeArrayFieldStart(name);
+                    for (Object v : (Collection) value) {
+                        writeProperty(name, v, false);
+                    }
+                    writer.writeEndArray();
+                } else {
+                    writeProperty(name, value, true);
+                }
+
+            } else if (propType instanceof Operation) {
+                writeProperty(name, value, true);
+            }
+        }
+
+        writer.writeEndObject();
+    }
+
+    /**
+     * Write a property (Complex or Simple)
+     *
+     * @param property
+     * @param writeFieldName
+     * @throws IOException
+     */
+    private void writeProperty(String name, Object value, boolean writeFieldName) throws IOException, IllegalArgumentException {
+        if (value instanceof Feature) {
+            writeProperties((Feature) value, name, writeFieldName);
+        } else {
+            writeAttribute(name, value, writeFieldName);
+        }
+    }
+
+    /**
+     * Write an Attribute and check if attribute value is assignable to binding
+     * class.
+     *
+     * @param property
+     * @param writeFieldName
+     * @throws IOException
+     */
+    private void writeAttribute(String name, Object value, boolean writeFieldName) throws IOException, IllegalArgumentException {
+        if (writeFieldName) {
+            writer.writeFieldName(name);
+        }
+        GeoJSONUtils.writeValue(value, writer);
+    }
+
+    /**
+     * Write a GeometryAttribute
+     *
+     * @param geom
+     * @throws IOException
+     */
+    void writeSingleGeometry(Attribute geom) throws IOException {
+        assert (!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "writeSingleGeometry can called only once per GeoJSONWriter.";
+        isSingleGeometry = true;
+        GeoJSONGeometry jsonGeometry = GeoJSONGeometry.toGeoJSONGeometry((Geometry) geom.getValue());
+        writeGeoJSONGeometry(jsonGeometry);
+    }
+
+    /**
+     * Write a JTS Geometry
+     *
+     * @param geom
+     * @throws IOException
+     */
+    void writeSingleGeometry(Geometry geom) throws IOException {
+        assert (!isFeatureCollection && !isSingleFeature && !isSingleGeometry) :
+                "writeSingleGeometry can called only once per GeoJSONWriter.";
+        isSingleGeometry = true;
+        GeoJSONGeometry jsonGeometry = GeoJSONGeometry.toGeoJSONGeometry(geom);
+        writeGeoJSONGeometry(jsonGeometry);
+    }
+
+    /**
+     * Write a GeometryAttribute
+     *
+     * @param geom
+     * @throws IOException
+     */
+    private void writeFeatureGeometry(Geometry geom) throws IOException {
+        writeGeoJSONGeometry(GeoJSONGeometry.toGeoJSONGeometry(geom));
+    }
+
+    /**
+     * Write a GeoJSONGeometry
+     *
+     * @param jsonGeometry
+     * @throws IOException
+     */
+    private void writeGeoJSONGeometry(GeoJSONGeometry jsonGeometry) throws IOException {
+        writer.writeStartObject();
+        writer.writeStringField(TYPE, jsonGeometry.getType());
+
+        if (jsonGeometry instanceof GeoJSONGeometryCollection) {
+            List<GeoJSONGeometry> geometries = ((GeoJSONGeometryCollection) jsonGeometry).getGeometries();
+            writer.writeArrayFieldStart(GEOMETRIES); // "geometries" : [
+            for (GeoJSONGeometry geometry : geometries) {
+                writeGeoJSONGeometry(geometry);
+            }
+            writer.writeEndArray(); // "]"
+        } else {
+            writer.writeArrayFieldStart(COORDINATES); // "coordinates" : [
+            if (jsonGeometry instanceof GeoJSONPoint) {
+                writeArray(((GeoJSONPoint) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONLineString) {
+                writeArray(((GeoJSONLineString) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONPolygon) {
+                writeArray(((GeoJSONPolygon) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONMultiPoint) {
+                writeArray(((GeoJSONMultiPoint) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONMultiLineString) {
+                writeArray(((GeoJSONMultiLineString) jsonGeometry).getCoordinates());
+            } else if (jsonGeometry instanceof GeoJSONMultiPolygon) {
+                writeArray(((GeoJSONMultiPolygon) jsonGeometry).getCoordinates());
+            } else {
+                throw new IllegalArgumentException("Unsupported geometry type : " + jsonGeometry);
+            }
+            writer.writeEndArray(); // "]"
+        }
+
+        writer.writeEndObject();
+    }
+
+    private void writeArray(double[] coordinates) throws IOException {
+        for (double coordinate : coordinates) {
+            writer.writeNumber(numberFormat.format(coordinate));
+        }
+    }
+
+    private void writeArray(double[][] coordinates) throws IOException {
+        for (double[] coordinate : coordinates) {
+            writer.writeStartArray(); // "["
+            writeArray(coordinate);
+            writer.writeEndArray(); // "]"
+        }
+    }
+
+    private void writeArray(double[][][] coordinates) throws IOException {
+        for (double[][] coordinate : coordinates) {
+            writer.writeStartArray(); // "["
+            writeArray(coordinate);
+            writer.writeEndArray(); // "]"
+        }
+    }
+
+    private void writeArray(double[][][][] coordinates) throws IOException {
+        for (double[][][] coordinate : coordinates) {
+            writer.writeStartArray(); // "["
+            writeArray(coordinate);
+            writer.writeEndArray(); // "]"
+        }
+    }
+
+    @Override
+    public void flush() throws IOException {
+        if (writer != null) {
+            writer.flush();
+        }
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (writer != null) {
+            writer.close();
+        }
+        if (outputStream != null) {
+            outputStream.close();
+        }
+    }
+}
diff --git a/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider b/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider
new file mode 100644
index 0000000..33f5d70
--- /dev/null
+++ b/storage/sis-geojson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider
@@ -0,0 +1 @@
+org.apache.sis.storage.geojson.GeoJSONProvider
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java
new file mode 100644
index 0000000..a2c2255
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/FeatureTypeUtilsTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.storage.geojson;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Date;
+import org.apache.sis.feature.FeatureComparator;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.internal.geojson.FeatureTypeUtils;
+import org.apache.sis.internal.geojson.GeoJSONUtils;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import static org.junit.Assert.*;
+import org.junit.Test;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+import org.opengis.util.FactoryException;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class FeatureTypeUtilsTest extends TestCase {
+
+    public static void main(String[] args) throws Exception {
+       new FeatureTypeUtilsTest().writeReadFTTest();
+    }
+
+    @Test
+    public void writeReadFTTest() throws Exception {
+
+        Path featureTypeFile = Files.createTempFile("complexFT", ".json");
+
+        FeatureType featureType = createComplexType();
+        FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile);
+
+        assertTrue(Files.size(featureTypeFile) > 0);
+
+        FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile);
+
+        assertNotNull(readFeatureType);
+        assertTrue(hasAGeometry(readFeatureType));
+        assertNotNull(GeoJSONUtils.getCRS(readFeatureType));
+
+        equalsIgnoreConvention(featureType, readFeatureType);
+    }
+
+    @Test
+    public void writeReadNoCRSFTTest() throws Exception {
+
+        Path featureTypeFile = Files.createTempFile("geomFTNC", ".json");
+
+        FeatureType featureType = createGeometryNoCRSFeatureType();
+        FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile);
+
+        assertTrue(Files.size(featureTypeFile) > 0);
+
+        FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile);
+
+        assertNotNull(readFeatureType);
+        assertTrue(hasAGeometry(readFeatureType));
+        assertNull(GeoJSONUtils.getCRS(readFeatureType));
+
+        equalsIgnoreConvention(featureType, readFeatureType);
+    }
+
+    @Test
+    public void writeReadCRSFTTest() throws Exception {
+
+        Path featureTypeFile = Files.createTempFile("geomFTC", ".json");
+
+        FeatureType featureType = createGeometryCRSFeatureType();
+        FeatureTypeUtils.writeFeatureType(featureType, featureTypeFile);
+
+        assertTrue(Files.size(featureTypeFile) > 0);
+
+        FeatureType readFeatureType = FeatureTypeUtils.readFeatureType(featureTypeFile);
+
+        assertNotNull(readFeatureType);
+        assertTrue(hasAGeometry(readFeatureType));
+        assertNotNull(GeoJSONUtils.getCRS(readFeatureType));
+
+        equalsIgnoreConvention(featureType, readFeatureType);
+    }
+
+    public static FeatureType createComplexType() throws FactoryException {
+        FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+
+        ftb.setName("complexAtt1");
+        ftb.addAttribute(Long.class).setName("longProp2");
+        ftb.addAttribute(String.class).setName("stringProp2");
+        final FeatureType complexAtt1 = ftb.build();
+
+        ftb = new FeatureTypeBuilder();
+        ftb.setName("complexAtt2");
+        ftb.addAttribute(Long.class).setName("longProp2");
+        ftb.addAttribute(Date.class).setName("dateProp");
+        final FeatureType complexAtt2 = ftb.build();
+
+        ftb = new FeatureTypeBuilder();
+        ftb.setName("complexFT");
+        ftb.addAttribute(Polygon.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(Long.class).setName("longProp");
+        ftb.addAttribute(String.class).setName("stringProp");
+        ftb.addAttribute(Integer.class).setName("integerProp");
+        ftb.addAttribute(Boolean.class).setName("booleanProp");
+        ftb.addAttribute(Date.class).setName("dateProp");
+
+        ftb.addAssociation(complexAtt1).setName("complexAtt1");
+        ftb.addAssociation(complexAtt2).setName("complexAtt2").setMinimumOccurs(0).setMaximumOccurs(Integer.MAX_VALUE);
+        ftb.setDescription(new SimpleInternationalString("Description"));
+        return ftb.build();
+    }
+
+    private FeatureType createGeometryNoCRSFeatureType() {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName("FT1");
+        ftb.addAttribute(Point.class).setName("geometry").addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(String.class).setName("type");
+
+        return ftb.build();
+    }
+
+    private FeatureType createGeometryCRSFeatureType() {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName("FT2");
+        ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.geographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(String.class).setName("type");
+
+        return ftb.build();
+    }
+
+    /**
+     * Loop on properties, returns true if there is at least one geometry property.
+     *
+     * @param type
+     * @return true if type has a geometry.
+     */
+    public static boolean hasAGeometry(FeatureType type) {
+        for (PropertyType pt : type.getProperties(true)){
+            if (AttributeConvention.isGeometryAttribute(pt)) return true;
+        }
+        return false;
+    }
+
+
+    /**
+     * Test field equality ignoring convention properties.
+     */
+    public static void equalsIgnoreConvention(FeatureType type1, FeatureType type2) {
+        final FeatureComparator comparator = new FeatureComparator(type1, type2);
+        comparator.ignoredProperties.add(AttributeConvention.IDENTIFIER);
+        comparator.ignoredProperties.add("identifier");
+        comparator.compare();
+    }
+
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java
new file mode 100644
index 0000000..8868e52
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONReadTest.java
@@ -0,0 +1,344 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.storage.geojson;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+import org.apache.sis.feature.FeatureComparator;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.geojson.GeoJSONParser;
+import org.apache.sis.internal.geojson.binding.GeoJSONFeatureCollection;
+import org.apache.sis.internal.geojson.binding.GeoJSONObject;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStores;
+import org.apache.sis.storage.WritableFeatureSet;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.util.iso.Names;
+import static org.junit.Assert.*;
+import org.junit.Test;
+import org.locationtech.jts.geom.*;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureType;
+import org.opengis.util.GenericName;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONReadTest extends TestCase {
+
+    @Test
+    public void readPointTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/point.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "point"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("point", Point.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readMultiPointTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multipoint.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "multipoint"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("multipoint", MultiPoint.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readLineStringTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/linestring.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "linestring"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("linestring", LineString.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readMultiLineStringTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multilinestring.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "multilinestring"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("multilinestring", MultiLineString.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readPolygonTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/polygon.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "polygon"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("polygon", Polygon.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readMultiPolygonTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/multipolygon.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "multipolygon"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("multipolygon", MultiPolygon.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readGeometryCollectionTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/geometrycollection.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "geometrycollection"), name);
+
+        testFeatureTypes(buildGeometryFeatureType("geometrycollection", GeometryCollection.class), ft);
+
+        assertEquals(1l, store.features(false).count());
+
+    }
+
+    @Test
+    public void readFeatureTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/feature.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "feature"), name);
+
+        testFeatureTypes(buildSimpleFeatureType("feature"), ft);
+
+        assertEquals(1l, store.features(false).count());
+    }
+
+    @Test
+    public void readFeatureCollectionTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "featurecollection"), name);
+
+        testFeatureTypes(buildFCFeatureType("featurecollection"), ft);
+
+        assertEquals(7l, store.features(false).count());
+    }
+
+    /**
+     * Test reading of Features with array as properties value
+     * @throws DataStoreException
+     */
+    @Test
+    public void readPropertyArrayTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/f_prop_array.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+        assertEquals(Names.createLocalName(null, null, "f_prop_array"), name);
+
+        testFeatureTypes(buildPropertyArrayFeatureType("f_prop_array", Geometry.class), ft);
+
+        assertEquals(2l, store.features(false).count());
+
+        Double[][] array1 = new Double[5][5];
+        Double[][] array2 = new Double[5][5];
+        for (int i = 0; i < 5; i++) {
+            for (int j = 0; j < 5; j++) {
+                array1[i][j] = (double) (i + j);
+                array2[i][j] = (double) (i - j);
+            }
+        }
+
+        Iterator<Feature> ite = store.features(false).iterator();
+        Feature feat1 = ite.next();
+        assertArrayEquals(array1, (Double[][]) feat1.getProperty("array").getValue());
+
+        Feature feat2 = ite.next();
+        assertArrayEquals(array2, (Double[][]) feat2.getProperty("array").getValue());
+
+    }
+
+    /**
+     * This test ensure that properties fields with null value doesn't rise NullPointerException
+     * @throws DataStoreException
+     */
+    @Test
+    public void readNullPropsTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+
+        assertEquals(15l, store.features(false).count());
+    }
+
+    /**
+     * This test ensure integer types over Integer.MAX_VALUE are converted to Long.
+     * @throws DataStoreException
+     */
+    @Test
+    public void readLongTest() throws DataStoreException, URISyntaxException {
+        URL file = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/longValue.json");
+
+        WritableFeatureSet store = (WritableFeatureSet) DataStores.open(file);
+        assertNotNull(store);
+
+        FeatureType ft = store.getType();
+        GenericName name = ft.getName();
+
+        Feature feature = store.features(false).findFirst().get();
+        assertEquals(853555090789l, feature.getPropertyValue("size"));
+    }
+
+    /**
+     * Test GeoJSONParser full and lazy reading on FeatureCollection
+     */
+    @Test
+    public void parserTest() throws URISyntaxException, IOException {
+        URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+        Path fcPath = Paths.get(fcFile.toURI());
+
+        // test with full reading
+        GeoJSONObject geoJSONObject = GeoJSONParser.parse(fcPath, false);
+        assertTrue(geoJSONObject instanceof GeoJSONFeatureCollection);
+        GeoJSONFeatureCollection geojsonFC = (GeoJSONFeatureCollection) geoJSONObject;
+        assertFalse(geojsonFC.isLazyMode());
+        assertEquals(7, geojsonFC.getFeatures().size());
+
+        for (int i = 0; i < 7; i++) {
+            assertTrue(geojsonFC.hasNext());
+            assertNotNull(geojsonFC.next());
+        }
+        assertFalse(geojsonFC.hasNext()); //end of collection
+
+
+        // test in lazy reading
+        geoJSONObject = GeoJSONParser.parse(fcPath, true);
+        assertTrue(geoJSONObject instanceof GeoJSONFeatureCollection);
+        geojsonFC = (GeoJSONFeatureCollection) geoJSONObject;
+        assertTrue(geojsonFC.isLazyMode());
+        assertEquals(0, geojsonFC.getFeatures().size()); //lazy don't know number of features
+
+        for (int i = 0; i < 7; i++) {
+            assertTrue(geojsonFC.hasNext());
+            assertNotNull(geojsonFC.next());
+        }
+        assertFalse(geojsonFC.hasNext()); //end of collection
+
+    }
+
+    private FeatureType buildPropertyArrayFeatureType(String name, Class<?> geomClass) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(Double[][].class).setName("array");
+        ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        return ftb.build();
+    }
+
+    private FeatureType buildGeometryFeatureType(String name, Class<?> geomClass) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(String.class).setName("fid").addRole(AttributeRole.IDENTIFIER_COMPONENT);
+        ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        return ftb.build();
+    }
+
+    private FeatureType buildSimpleFeatureType(String name) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(Polygon.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(String.class).setName("name");
+        return ftb.build();
+    }
+
+    private FeatureType buildFCFeatureType(String name) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(Geometry.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.addAttribute(String.class).setName("name");
+        ftb.addAttribute(String.class).setName("address");
+        return ftb.build();
+    }
+
+    private void testFeatureTypes(FeatureType expected, FeatureType result) {
+        final FeatureComparator comparator = new FeatureComparator(expected, result);
+        comparator.compare();
+    }
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java
new file mode 100644
index 0000000..55b0028
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/GeoJSONWriteTest.java
@@ -0,0 +1,581 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.storage.geojson;
+
+import org.apache.sis.feature.FeatureComparator;
+import com.fasterxml.jackson.core.JsonEncoding;
+import java.io.*;
+import java.lang.reflect.Array;
+import java.net.URI;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.stream.Stream;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.feature.builder.AttributeRole;
+import org.apache.sis.feature.builder.FeatureTypeBuilder;
+import org.apache.sis.internal.feature.AttributeConvention;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.WritableFeatureSet;
+import org.apache.sis.storage.geojson.GeoJSONProvider;
+import org.apache.sis.storage.geojson.GeoJSONStore;
+import org.apache.sis.storage.geojson.GeoJSONStreamWriter;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.test.TestCase;
+import static org.junit.Assert.*;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.locationtech.jts.geom.*;
+import org.locationtech.jts.io.WKTReader;
+import org.opengis.feature.AttributeType;
+import org.opengis.feature.Feature;
+import org.opengis.feature.FeatureAssociationRole;
+import org.opengis.feature.FeatureType;
+import org.opengis.feature.PropertyType;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class GeoJSONWriteTest extends TestCase {
+
+    private static final GeometryFactory GF = new GeometryFactory();
+    private static final WKTReader WKT_READER = new WKTReader();
+    private static final Properties PROPERTIES = new Properties();
+
+    @BeforeClass
+    public static void init() throws IOException {
+        PROPERTIES.load(GeoJSONWriteTest.class.getResourceAsStream("/org/apache/sis/internal/storage/geojson/geometries.properties"));
+    }
+
+    @Test
+    public void writeSimpleFTTest() throws Exception {
+
+        final Path file = Files.createTempFile("point", ".json");
+
+        final WritableFeatureSet store = new GeoJSONStore(new GeoJSONProvider(), file, 7);
+        assertNotNull(store);
+        final String typeName = file.getFileName().toString().replace(".json", "");
+
+
+        //test creating an unvalid feature type
+        final FeatureType unvalidFeatureType = buildGeometryFeatureType("test", Point.class);
+        try {
+            store.updateType(unvalidFeatureType);
+            fail();
+        } catch (DataStoreException ex) {
+            //normal exception
+        }
+
+
+        //test writing and reading a feature
+        final FeatureType validFeatureType = buildGeometryFeatureType(typeName, Point.class);
+        store.updateType(validFeatureType);
+        assertNotNull(store.getType());
+        assertTrue(Files.exists(file));
+
+        final Point expectedPoint = GF.createPoint(new Coordinate(-105.01621, 39.57422));
+        final Feature feature = store.getType().newInstance();
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), expectedPoint);
+        feature.setPropertyValue("type","simple");
+        store.add(Arrays.asList(feature).iterator());
+
+        assertTrue(Files.exists(file));
+
+
+        try (Stream<Feature> stream = store.features(false)) {
+            final Iterator<Feature> reader = stream.iterator();
+            assertTrue(reader.hasNext());
+            final Feature f = reader.next();
+            assertEquals("simple", f.getPropertyValue("type"));
+            assertEquals(expectedPoint, f.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString()));
+        }
+
+        Files.deleteIfExists(file);
+    }
+
+    @Test
+    public void writeAbstractGeometryTest() throws Exception {
+
+        Path file = Files.createTempFile("geoms", ".json");
+
+        WritableFeatureSet store = new GeoJSONStore(new GeoJSONProvider(), file, 7);
+        assertNotNull(store);
+
+        String typeName = file.getFileName().toString().replace(".json", "");
+        FeatureType validFeatureType = buildGeometryFeatureType(typeName, Geometry.class);
+
+        store.updateType(validFeatureType);
+        assertNotNull(store.getType());
+        assertTrue(Files.exists(file));
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+        MultiPoint mpt = (MultiPoint)WKT_READER.read(PROPERTIES.getProperty("multipoint"));
+        LineString line = (LineString)WKT_READER.read(PROPERTIES.getProperty("linestring"));
+        MultiLineString mline = (MultiLineString)WKT_READER.read(PROPERTIES.getProperty("multilinestring"));
+        Polygon poly = (Polygon)WKT_READER.read(PROPERTIES.getProperty("polygon"));
+        MultiPolygon mpoly = (MultiPolygon)WKT_READER.read(PROPERTIES.getProperty("multipolygon"));
+        GeometryCollection coll = (GeometryCollection)WKT_READER.read(PROPERTIES.getProperty("geometrycollection"));
+
+        Feature feature = store.getType().newInstance();
+        feature.setPropertyValue("type","Point");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","MultiPoint");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mpt);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","LineString");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), line);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","MultiLineString");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mline);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","Polygon");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), poly);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","MultiPolygon");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), mpoly);
+        store.add(Arrays.asList(feature).iterator());
+
+        feature = store.getType().newInstance();
+        feature.setPropertyValue("type","GeometryCollection");
+        feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), coll);
+        store.add(Arrays.asList(feature).iterator());
+
+        assertTrue(Files.exists(file));
+
+        assertEquals(7, store.features(false).count());
+
+        try (Stream<Feature> stream = store.features(false)) {
+            Iterator<Feature> ite = stream.iterator();
+            while (ite.hasNext()) {
+                Feature f = ite.next();
+                //System.out.println(f);
+                Geometry geom = (Geometry)f.getPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString());
+
+                if (geom instanceof Point) {
+                    assertTrue(pt.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof MultiPoint) {
+                    assertTrue(mpt.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof LineString) {
+                    assertTrue(line.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof MultiLineString) {
+                    assertTrue(mline.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof Polygon) {
+                    assertTrue(poly.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof MultiPolygon) {
+                    assertTrue(mpoly.equalsExact(geom, 0.0000001));
+                } else if (geom instanceof GeometryCollection) {
+                    assertTrue(coll.equalsExact(geom, 0.0000001));
+                }
+            }
+        }
+
+        Files.deleteIfExists(file);
+    }
+
+    @Test
+    public void writeComplexFeaturesTest() throws Exception {
+        Path file = Files.createTempFile("complex", ".json");
+
+        WritableFeatureSet store = new GeoJSONStore(new GeoJSONProvider(), file, 7);
+        assertNotNull(store);
+
+        String typeName = file.getFileName().toString().replace(".json", "");
+
+        FeatureType complexFT = buildComplexFeatureType(typeName);
+
+        store.updateType(complexFT);
+        assertNotNull(store.getType());
+        assertTrue(Files.exists(file));
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+        Feature expected = null;
+        Feature feature = store.getType().newInstance();
+        feature.setPropertyValue("longProp",100l);
+        feature.setPropertyValue("stringProp","Some String");
+        feature.setPropertyValue("integerProp",15);
+        feature.setPropertyValue("booleanProp",true);
+
+        final FeatureType level1Type = ((FeatureAssociationRole)feature.getType().getProperty("level1")).getValueType();
+
+        final Feature level11 = level1Type.newInstance();
+        level11.setPropertyValue("longProp2",66446l);
+
+        final FeatureType level2Type = ((FeatureAssociationRole)level11.getType().getProperty("level2")).getValueType();
+
+        final Feature level211 = level2Type.newInstance();
+        level211.setPropertyValue("level2prop","text");
+        final Feature level212 = level2Type.newInstance();
+        level212.setPropertyValue("level2prop","text2");
+        final Feature level213 = level2Type.newInstance();
+        level213.setPropertyValue("level2prop","text3");
+
+        level11.setPropertyValue("level2", Arrays.asList(level211,level212,level213));
+
+
+        Feature level12 = level1Type.newInstance();
+        level12.setPropertyValue("longProp2",4444444l);
+
+        final Feature level221 = level2Type.newInstance();
+        level221.setPropertyValue("level2prop","fish");
+        final Feature level222 = level2Type.newInstance();
+        level222.setPropertyValue("level2prop","cat");
+        final Feature level223 = level2Type.newInstance();
+        level223.setPropertyValue("level2prop","dog");
+
+        level12.setPropertyValue("level2", Arrays.asList(level221,level222,level223));
+
+        feature.setPropertyValue("level1",Arrays.asList(level11,level12));
+
+        feature.setPropertyValue("geometry", pt);
+        expected = copy(feature);
+        store.add(Arrays.asList(feature).iterator());
+
+
+        assertTrue(Files.exists(file));
+
+        assertEquals(1, store.features(false).count());
+
+        try (Stream<Feature> stream = store.features(false)) {
+            Iterator<Feature> ite = stream.iterator();
+            while (ite.hasNext()) {
+                Feature candidate = ite.next();
+                FeatureComparator comparator = new FeatureComparator(expected, candidate);
+                comparator.ignoredProperties.add(AttributeConvention.IDENTIFIER);
+                comparator.compare();
+            }
+        }
+        Files.deleteIfExists(file);
+    }
+
+    @Test
+    public void writeStreamTest() throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        FeatureType validFeatureType = buildGeometryFeatureType("simpleFT", Point.class);
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+
+        try (GeoJSONStreamWriter fw = new GeoJSONStreamWriter(baos, validFeatureType, 4)) {
+            Feature feature = fw.next();
+            feature.setPropertyValue("type","feat1");
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            fw.write();
+
+            feature = fw.next();
+            feature.setPropertyValue("type","feat2");
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            fw.write();
+
+        }
+
+        String outputJSON = baos.toString("UTF-8");
+        assertNotNull(outputJSON);
+        assertFalse(outputJSON.isEmpty());
+
+        String expected = "{\n" +
+                "\"type\":\"FeatureCollection\"\n" +
+                ",\"features\":[\n" +
+                "{\"type\":\"Feature\",\"id\":0,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"type\":\"feat1\"}}\n" +
+                ",{\"type\":\"Feature\",\"id\":1,\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"type\":\"feat2\"}}\n" +
+                "]}";
+
+        assertEquals(expected, outputJSON);
+    }
+
+    @Test
+    public void writeStreamSingleFeatureTest() throws Exception {
+        FeatureType validFeatureType = buildGeometryFeatureType("simpleFT", Point.class);
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+
+        final String outputJSON;
+        try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            Feature feature = validFeatureType.newInstance();
+            feature.setPropertyValue(AttributeConvention.IDENTIFIER_PROPERTY.toString(), 0);
+            feature.setPropertyValue("type","feat1");
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            GeoJSONStreamWriter.writeSingleFeature(baos, feature, JsonEncoding.UTF8, 4, false);
+
+            outputJSON = baos.toString("UTF-8");
+        }
+
+        assertNotNull(outputJSON);
+        assertFalse(outputJSON.isEmpty());
+
+        String expected = "{\"type\":\"Feature\",\"id\":0," +
+                "\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]}," +
+                "\"properties\":{\"type\":\"feat1\"}}";
+        assertEquals(expected, outputJSON);
+    }
+
+    @Test
+    public void writeStreamSingleGeometryTest() throws Exception {
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+
+        final String outputJSON;
+        try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            GeoJSONStreamWriter.writeSingleGeometry(baos, pt, JsonEncoding.UTF8, 4, false);
+
+            outputJSON = baos.toString("UTF-8");
+        }
+
+        assertNotNull(outputJSON);
+        assertFalse(outputJSON.isEmpty());
+
+        String expected = "{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]}";
+        assertEquals(expected, outputJSON);
+    }
+
+    @Test
+    public void writePropertyArrayTest() throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        FeatureType validFeatureType = buildPropertyArrayFeatureType("arrayFT", Point.class);
+
+        Point pt = (Point)WKT_READER.read(PROPERTIES.getProperty("point"));
+
+        double[][] array1 = new double[5][5];
+        double[][] array2 = new double[5][5];
+        for (int i = 0; i < 5; i++) {
+            for (int j = 0; j < 5; j++) {
+                array1[i][j] = i+j;
+                array2[i][j] = i-j;
+            }
+        }
+
+        try (GeoJSONStreamWriter fw = new GeoJSONStreamWriter(baos, validFeatureType, 4)) {
+            Feature feature = fw.next();
+            feature.setPropertyValue("array",array1);
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            fw.write();
+
+            feature = fw.next();
+            feature.setPropertyValue("array",array2);
+            feature.setPropertyValue(AttributeConvention.GEOMETRY_PROPERTY.toString(), pt);
+            fw.write();
+
+        }
+
+        String outputJSON = baos.toString("UTF-8");
+        assertNotNull(outputJSON);
+        assertFalse(outputJSON.isEmpty());
+
+        String expected = "{\n" +
+                "\"type\":\"FeatureCollection\"\n" +
+                ",\"features\":[\n" +
+                "{\"type\":\"Feature\",\"id\":\"0\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"array\":[[0.0,1.0,2.0,3.0,4.0],[1.0,2.0,3.0,4.0,5.0],[2.0,3.0,4.0,5.0,6.0],[3.0,4.0,5.0,6.0,7.0],[4.0,5.0,6.0,7.0,8.0]]}}\n" +
+                ",{\"type\":\"Feature\",\"id\":\"1\",\"geometry\":{\"type\":\"Point\",\"coordinates\":[-105.0162,39.5742]},\"properties\":{\"array\":[[0.0,-1.0,-2.0,-3.0,-4.0],[1.0,0.0,-1.0,-2.0,-3.0],[2.0,1.0,0.0,-1.0,-2.0],[3.0,2.0,1.0,0.0,-1.0],[4.0,3.0,2.0,1.0,0.0]]}}\n" +
+                "]}";
+        assertEquals(expected, outputJSON);
+    }
+
+    private FeatureType buildGeometryFeatureType(String name, Class<?> geomClass) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(Integer.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(String.class).setName("type");
+        ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        return ftb.build();
+    }
+
+    private FeatureType buildPropertyArrayFeatureType(String name, Class<?> geomClass) {
+        final FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(double[][].class).setName("array");
+        ftb.addAttribute(geomClass).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        return ftb.build();
+    }
+
+    /**
+     *  Build 2 level Feature complex
+     */
+    private FeatureType buildComplexFeatureType(String name) {
+        FeatureTypeBuilder ftb = new FeatureTypeBuilder();
+
+        ftb.setName("level2");
+        ftb.addAttribute(String.class).setName("level2prop");
+        final FeatureType level2 = ftb.build();
+
+        ftb = new FeatureTypeBuilder();
+        ftb.setName("level1");
+        ftb.addAttribute(Long.class).setName("longProp2");
+        ftb.addAssociation(level2).setName("level2").setMinimumOccurs(1).setMaximumOccurs(5);
+        final FeatureType level1 = ftb.build();
+
+        ftb = new FeatureTypeBuilder();
+        ftb.setName(name);
+        ftb.addAttribute(String.class).setName(AttributeConvention.IDENTIFIER_PROPERTY);
+        ftb.addAttribute(Long.class).setName("longProp");
+        ftb.addAttribute(String.class).setName("stringProp");
+        ftb.addAttribute(Integer.class).setName("integerProp");
+        ftb.addAttribute(Boolean.class).setName("booleanProp");
+        ftb.addAssociation(level1).setName("level1").setMinimumOccurs(1).setMaximumOccurs(3);
+        ftb.addAttribute(Point.class).setName("geometry").setCRS(CommonCRS.WGS84.normalizedGeographic()).addRole(AttributeRole.DEFAULT_GEOMETRY);
+        ftb.setDescription(new SimpleInternationalString("Description"));
+        return ftb.build();
+    }
+
+    /**
+     * Create a copy of given feature.
+     * This is not a deep copy, only the feature and associated feature are copied,
+     * values are not copied.
+     */
+    public static Feature copy(Feature feature){
+        return copy(feature, false);
+    }
+
+    /**
+     * @param deep true for a deep copy
+     */
+    private static Feature copy(Feature feature, boolean deep){
+        final FeatureType type = feature.getType();
+
+        final Feature cp = type.newInstance();
+
+        final Collection<? extends PropertyType> props = type.getProperties(true);
+        for (PropertyType pt : props) {
+            if (pt instanceof AttributeType ){
+                final String name = pt.getName().toString();
+                final Object val = feature.getPropertyValue(name);
+                if(val!=null){
+                    cp.setPropertyValue(name, deep ? deepCopy(val) : val);
+                }
+            } else if(pt instanceof FeatureAssociationRole) {
+                final String name = pt.getName().toString();
+                final Object val = feature.getPropertyValue(name);
+                if (deep) {
+                    if(val!=null){
+                        cp.setPropertyValue(name, deepCopy(val));
+                    }
+                } else {
+                    if(val instanceof Collection){
+                        final Collection col = (Collection) val;
+                        final Collection cpCol = new ArrayList(col.size());
+                        for(Iterator ite=col.iterator();ite.hasNext();){
+                            cpCol.add(copy((Feature)ite.next()));
+                        }
+                        cp.setPropertyValue(name, cpCol);
+                    }else if(val!=null){
+                        cp.setPropertyValue(name, copy((Feature)val));
+                    }
+                }
+
+            }
+        }
+        return cp;
+    }
+
+    /**
+     * Make a deep copy of given Feature.
+     *
+     * @param feature Feature to copy
+     * @return Deep copy of the feature
+     */
+    public static Feature deepCopy(Feature feature){
+        return copy(feature, true);
+    }
+
+    /**
+     * Make a copy of given object.
+     * Multiplace cases are tested to make a deep copy.
+     *
+     * @param candidate
+     * @return copied object
+     */
+    public static Object deepCopy(final Object candidate) {
+        if(candidate==null) return null;
+
+        if(candidate instanceof String ||
+           candidate instanceof Number ||
+           candidate instanceof URL ||
+           candidate instanceof URI ||
+           candidate.getClass().isPrimitive() ||
+           candidate instanceof Character ||
+           candidate instanceof GridCoverage){
+            //we consider those immutable
+            return candidate;
+        }else if(candidate instanceof Feature){
+            return deepCopy((Feature)candidate);
+        }else if(candidate instanceof Geometry){
+            return ((Geometry)candidate).clone();
+        }else if(candidate instanceof Date){
+            return ((Date)candidate).clone();
+        }else if(candidate instanceof Date){
+            return ((Date)candidate).clone();
+        }else if(candidate instanceof Object[]){
+            final Object[] array = (Object[])candidate;
+            final Object[] copy = new Object[array.length];
+            for (int i = 0; i < array.length; i++) {
+                copy[i] = deepCopy(array[i]);
+            }
+            return copy;
+        }else if(candidate instanceof List){
+            final List list = (List)candidate;
+            final int size = list.size();
+            final List cp = new ArrayList(size);
+            for(int i=0;i<size;i++){
+                cp.add(deepCopy(list.get(i)));
+            }
+            return cp;
+        }else if (candidate instanceof Map) {
+            final Map map = (Map) candidate;
+            final Map cp = new HashMap(map.size());
+            for(final Iterator<Map.Entry> ite=map.entrySet().iterator(); ite.hasNext();) {
+                final Map.Entry entry = ite.next();
+                cp.put(entry.getKey(), deepCopy(entry.getValue()));
+            }
+            return Collections.unmodifiableMap(cp);
+        }
+
+        //array type
+        final Class clazz = candidate.getClass();
+        if(clazz.isArray()){
+            final Class compClazz = clazz.getComponentType();
+            final int length = Array.getLength(candidate);
+            final Object cp = Array.newInstance(compClazz, length);
+
+            if(compClazz.isPrimitive()){
+                System.arraycopy(candidate, 0, cp, 0, length);
+            }else{
+                for(int i=0;i<length; i++){
+                    Array.set(cp, i, deepCopy(Array.get(candidate, i)));
+                }
+            }
+            return cp;
+        }
+
+        //could not copy
+        return candidate;
+    }
+
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.java b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.java
new file mode 100644
index 0000000..94c43a3
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/internal/storage/geojson/LiteJsonLocationTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.internal.storage.geojson;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.core.JsonParser;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.apache.sis.internal.geojson.GeoJSONParser;
+import org.apache.sis.internal.geojson.GeoJSONUtils;
+import org.apache.sis.internal.geojson.LiteJsonLocation;
+import org.apache.sis.test.TestCase;
+
+/**
+ * @author Quentin Boileau (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 2.0
+ * @since   2.0
+ * @module
+ */
+public class LiteJsonLocationTest extends TestCase {
+
+    @Test
+    public void testEquality () throws URISyntaxException, IOException {
+        URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+        Path fcPath = Paths.get(fcFile.toURI());
+
+        JsonLocation streamLocation = null;
+        JsonLocation readerLocation = null;
+
+        //get Location from stream
+        try (InputStream stream = Files.newInputStream(fcPath);
+             JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) {
+            streamLocation = moveAndReturnPos(parser);
+        }
+
+        //get Location from reader
+        try (BufferedReader reader = Files.newBufferedReader(fcPath, Charset.forName("UTF-8"));
+             JsonParser parser = GeoJSONParser.FACTORY.createParser(reader)) {
+            readerLocation = moveAndReturnPos(parser);
+        }
+
+        Assert.assertFalse(streamLocation.equals(readerLocation));
+        Assert.assertFalse(GeoJSONUtils.equals(streamLocation, readerLocation));
+
+        LiteJsonLocation liteStreamLocation = new LiteJsonLocation(streamLocation);
+        LiteJsonLocation liteReaderLocation = new LiteJsonLocation(readerLocation);
+
+        Assert.assertTrue(liteStreamLocation.equals(liteReaderLocation));
+
+        Assert.assertTrue(liteStreamLocation.equals(streamLocation));
+        Assert.assertTrue(liteStreamLocation.equals(readerLocation));
+
+        Assert.assertTrue(liteReaderLocation.equals(streamLocation));
+        Assert.assertTrue(liteReaderLocation.equals(readerLocation));
+
+    }
+
+
+    @Test
+    public void testBefore () throws URISyntaxException, IOException {
+        URL fcFile = GeoJSONReadTest.class.getResource("/org/apache/sis/internal/storage/geojson/featurecollection.json");
+        Path fcPath = Paths.get(fcFile.toURI());
+
+        //get Location from stream
+        try (InputStream stream = Files.newInputStream(fcPath);
+             JsonParser parser = GeoJSONParser.FACTORY.createParser(stream)) {
+            parser.nextToken();
+
+            JsonLocation currentLocation = parser.getCurrentLocation();
+            LiteJsonLocation liteJsonLocation = new LiteJsonLocation(currentLocation);
+            Assert.assertFalse(liteJsonLocation.isBefore(currentLocation));
+
+            parser.nextToken();
+            currentLocation = parser.getCurrentLocation();
+            Assert.assertTrue(liteJsonLocation.isBefore(currentLocation));
+        }
+    }
+
+
+    private JsonLocation moveAndReturnPos(JsonParser parser) throws IOException {
+        parser.nextToken();
+        parser.nextToken();
+        parser.nextToken();
+        parser.nextToken();
+        return parser.getCurrentLocation();
+    }
+}
diff --git a/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java b/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java
new file mode 100644
index 0000000..175eaee
--- /dev/null
+++ b/storage/sis-geojson/src/test/java/org/apache/sis/test/suite/GeoJSONTestSuite.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sis.test.suite;
+
+import org.apache.sis.test.TestSuite;
+import org.junit.runners.Suite;
+import org.junit.BeforeClass;
+
+
+/**
+ * All tests from the {@code sis-geojson} module, in rough dependency order.
+ */
+@Suite.SuiteClasses({
+    org.apache.sis.internal.storage.geojson.FeatureTypeUtilsTest.class,
+    org.apache.sis.internal.storage.geojson.GeoJSONReadTest.class,
+    org.apache.sis.internal.storage.geojson.GeoJSONWriteTest.class,
+    org.apache.sis.internal.storage.geojson.LiteJsonLocationTest.class
+})
+public final strictfp class GeoJSONTestSuite extends TestSuite {
+}
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json
new file mode 100644
index 0000000..4ca0f62
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/f_prop_array.json
@@ -0,0 +1,7 @@
+{
+    "type":"FeatureCollection"
+    ,"crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}}
+    ,"features":[
+    {"type":"Feature","id":"id-0","geometry":{"type":"Point","coordinates":[-105.0162,39.5742]},"properties":{"array":[[0.0,1.0,2.0,3.0,4.0],[1.0,2.0,3.0,4.0,5.0],[2.0,3.0,4.0,5.0,6.0],[3.0,4.0,5.0,6.0,7.0],[4.0,5.0,6.0,7.0,8.0]]}}
+    ,{"type":"Feature","id":"id-1","geometry":{"type":"Point","coordinates":[-105.0162,39.5742]},"properties":{"array":[[0.0,-1.0,-2.0,-3.0,-4.0],[1.0,0.0,-1.0,-2.0,-3.0],[2.0,1.0,0.0,-1.0,-2.0],[3.0,2.0,1.0,0.0,-1.0],[4.0,3.0,2.0,1.0,0.0]]}}
+]}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json
new file mode 100644
index 0000000..248f582
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/feature.json
@@ -0,0 +1,69 @@
+ {
+    "type": "Feature",
+    "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+            [
+                [
+                    -80.72487831115721,
+                    35.26545403190955
+                ],
+                [
+                    -80.72135925292969,
+                    35.26727607954368
+                ],
+                [
+                    -80.71517944335938,
+                    35.26769654625573
+                ],
+                [
+                    -80.7125186920166,
+                    35.27035945142482
+                ],
+                [
+                    -80.70857048034668,
+                    35.268257165144064
+                ],
+                [
+                    -80.70479393005371,
+                    35.268397319259996
+                ],
+                [
+                    -80.70324897766113,
+                    35.26503355355979
+                ],
+                [
+                    -80.71088790893555,
+                    35.2553619492954
+                ],
+                [
+                    -80.71681022644043,
+                    35.2553619492954
+                ],
+                [
+                    -80.7150936126709,
+                    35.26054831539319
+                ],
+                [
+                    -80.71869850158691,
+                    35.26026797976481
+                ],
+                [
+                    -80.72032928466797,
+                    35.26061839914875
+                ],
+                [
+                    -80.72264671325684,
+                    35.26033806376283
+                ],
+                [
+                    -80.72487831115721,
+                    35.26545403190955
+                ]
+            ]
+        ]
+    },
+    "properties": {
+        "name": "Plaza Road Park"
+    }
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json
new file mode 100644
index 0000000..e4d7ae4
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/featurecollection.json
@@ -0,0 +1,158 @@
+{
+    "type": "FeatureCollection",
+    "features": [
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.87088507656375,
+                    35.21515162500578
+                ]
+            },
+            "properties": {
+                "name": "ABBOTT NEIGHBORHOOD PARK",
+                "address": "1300  SPRUCE ST"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.83775386582222,
+                    35.24980190252168
+                ]
+            },
+            "properties": {
+                "name": "DOUBLE OAKS CENTER",
+                "address": "1326 WOODWARD AV"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.83827000459532,
+                    35.25674709224663
+                ]
+            },
+            "properties": {
+                "name": "DOUBLE OAKS NEIGHBORHOOD PARK",
+                "address": "2605  DOUBLE OAKS RD"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.83697759172735,
+                    35.25751734669229
+                ]
+            },
+            "properties": {
+                "name": "DOUBLE OAKS POOL",
+                "address": "1200 NEWLAND RD"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.81647652154736,
+                    35.40148708491418
+                ]
+            },
+            "properties": {
+                "name": "DAVID B. WAYMER FLYING REGIONAL PARK",
+                "address": "15401 HOLBROOKS RD"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Point",
+                "coordinates": [
+                    -80.83556459443902,
+                    35.39917224760999
+                ]
+            },
+            "properties": {
+                "name": "DAVID B. WAYMER COMMUNITY PARK",
+                "address": "302 HOLBROOKS RD"
+            }
+        },
+        {
+            "type": "Feature",
+            "geometry": {
+                "type": "Polygon",
+                "coordinates": [
+                    [
+                        [
+                            -80.72487831115721,
+                            35.26545403190955
+                        ],
+                        [
+                            -80.72135925292969,
+                            35.26727607954368
+                        ],
+                        [
+                            -80.71517944335938,
+                            35.26769654625573
+                        ],
+                        [
+                            -80.7125186920166,
+                            35.27035945142482
+                        ],
+                        [
+                            -80.70857048034668,
+                            35.268257165144064
+                        ],
+                        [
+                            -80.70479393005371,
+                            35.268397319259996
+                        ],
+                        [
+                            -80.70324897766113,
+                            35.26503355355979
+                        ],
+                        [
+                            -80.71088790893555,
+                            35.2553619492954
+                        ],
+                        [
+                            -80.71681022644043,
+                            35.2553619492954
+                        ],
+                        [
+                            -80.7150936126709,
+                            35.26054831539319
+                        ],
+                        [
+                            -80.71869850158691,
+                            35.26026797976481
+                        ],
+                        [
+                            -80.72032928466797,
+                            35.26061839914875
+                        ],
+                        [
+                            -80.72264671325684,
+                            35.26033806376283
+                        ],
+                        [
+                            -80.72487831115721,
+                            35.26545403190955
+                        ]
+                    ]
+                ]
+            },
+            "properties": {
+                "name": "Plaza Road Park"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties
new file mode 100644
index 0000000..f638ee6
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometries.properties
@@ -0,0 +1,8 @@
+# WKT geometries for test purpose
+point=POINT(-105.01621 39.57422)
+multipoint=MULTIPOINT((-105.01621 39.57422),(-80.6665134 35.0539943))
+linestring=LINESTRING(-101.744384765625 39.32155002466662,-101.5521240234375 39.330048552942415,-101.40380859375 39.330048552942415,-101.33239746093749 39.364032338047984,-101.041259765625 39.36827914916011,-100.975341796875 39.30454987014581,-100.9149169921875 39.24501680713314,-100.843505859375 39.16414104768742,-100.8050537109375 39.104488809440475,-100.491943359375 39.10022600175347,-100.43701171875 39.095962936305476,-100.338134765625 39.095962936305476,-100.1953125 39.0277188402116 [...]
+multilinestring=MULTILINESTRING((-105.0214433670044 39.57805759162015,-105.02150774002075 39.57780951131517,-105.02157211303711 39.57749527498758,-105.02157211303711 39.57716449836683,-105.02157211303711 39.57703218727656,-105.02152919769287 39.57678410330158),(-105.01989841461182 39.574997872470774,-105.01959800720215 39.57489863607502,-105.01906156539916 39.57478286010041),(-105.01717329025269 39.5744024519653,-105.01698017120361 39.574385912433804,-105.0166368484497 39.574385912433804 [...]
+polygon=POLYGON((-84.32281494140625 34.9895035675793,-84.29122924804688 35.21981940793435,-84.24041748046875 35.25459097465022,-84.22531127929688 35.266925688950074,-84.20745849609375 35.26580442886754,-84.19921875 35.24674063355999,-84.16213989257812 35.24113278166642,-84.12368774414062 35.24898366572645,-84.09072875976562 35.24898366572645,-84.08798217773438 35.264683153268116,-84.04266357421875 35.27701633139884,-84.03030395507812 35.291589484566124,-84.0234375 35.306160014550784,-84. [...]
+multipolygon=MULTIPOLYGON(((-84.32281494140625 34.9895035675793,-84.29122924804688 35.21981940793435,-84.24041748046875 35.25459097465022,-84.22531127929688 35.266925688950074,-84.20745849609375 35.26580442886754,-84.19921875 35.24674063355999,-84.16213989257812 35.24113278166642,-84.12368774414062 35.24898366572645,-84.09072875976562 35.24898366572645,-84.08798217773438 35.264683153268116,-84.04266357421875 35.27701633139884,-84.03030395507812 35.291589484566124,-84.0234375 35.306160014 [...]
+geometrycollection=GEOMETRYCOLLECTION(POINT(-80.66080570220947 35.04939206472683),POLYGON((-80.66458225250244 35.04496519190309,-80.66344499588013 35.04603679820616,-80.66258668899536 35.045580049697556,-80.66387414932251 35.044280059194946,-80.66458225250244 35.04496519190309)),LINESTRING(-80.66237211227417 35.05950973022538,-80.66269397735596 35.0592638296087,-80.66284418106079 35.05893010615862,-80.66308021545409 35.05833291342246,-80.66359519958496 35.057753281001425,-80.663874149322 [...]
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json
new file mode 100644
index 0000000..6287457
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/geometrycollection.json
@@ -0,0 +1,188 @@
+{
+    "type": "GeometryCollection",
+    "geometries": [
+        {
+            "type": "Point",
+            "coordinates": [
+                -80.66080570220947,
+                35.04939206472683
+            ]
+        },
+        {
+            "type": "Polygon",
+            "coordinates": [
+                [
+                    [
+                        -80.66458225250244,
+                        35.04496519190309
+                    ],
+                    [
+                        -80.66344499588013,
+                        35.04603679820616
+                    ],
+                    [
+                        -80.66258668899536,
+                        35.045580049697556
+                    ],
+                    [
+                        -80.66387414932251,
+                        35.044280059194946
+                    ],
+                    [
+                        -80.66458225250244,
+                        35.04496519190309
+                    ]
+                ]
+            ]
+        },
+        {
+            "type": "LineString",
+            "coordinates": [
+                [
+                    -80.66237211227417,
+                    35.05950973022538
+                ],
+                [
+                    -80.66269397735596,
+                    35.0592638296087
+                ],
+                [
+                    -80.66284418106079,
+                    35.05893010615862
+                ],
+                [
+                    -80.66308021545409,
+                    35.05833291342246
+                ],
+                [
+                    -80.66359519958496,
+                    35.057753281001425
+                ],
+                [
+                    -80.66387414932251,
+                    35.05740198662245
+                ],
+                [
+                    -80.66441059112549,
+                    35.05703312589789
+                ],
+                [
+                    -80.66486120223999,
+                    35.056787217822475
+                ],
+                [
+                    -80.66541910171509,
+                    35.05650617911516
+                ],
+                [
+                    -80.66563367843628,
+                    35.05631296444281
+                ],
+                [
+                    -80.66601991653441,
+                    35.055891403570705
+                ],
+                [
+                    -80.66619157791138,
+                    35.05545227534804
+                ],
+                [
+                    -80.66619157791138,
+                    35.05517123204622
+                ],
+                [
+                    -80.66625595092773,
+                    35.05489018777713
+                ],
+                [
+                    -80.6662130355835,
+                    35.054222703761525
+                ],
+                [
+                    -80.6662130355835,
+                    35.05392409072499
+                ],
+                [
+                    -80.66595554351807,
+                    35.05290528508858
+                ],
+                [
+                    -80.66569805145262,
+                    35.052044560077285
+                ],
+                [
+                    -80.66550493240356,
+                    35.0514824490509
+                ],
+                [
+                    -80.665762424469,
+                    35.05048117920187
+                ],
+                [
+                    -80.66617012023926,
+                    35.04972582715769
+                ],
+                [
+                    -80.66651344299316,
+                    35.049286665781096
+                ],
+                [
+                    -80.66692113876343,
+                    35.0485313026898
+                ],
+                [
+                    -80.66700696945189,
+                    35.048215102112344
+                ],
+                [
+                    -80.66707134246826,
+                    35.04777593261294
+                ],
+                [
+                    -80.66704988479614,
+                    35.04738946150025
+                ],
+                [
+                    -80.66696405410767,
+                    35.04698542156371
+                ],
+                [
+                    -80.66681385040283,
+                    35.046353007216055
+                ],
+                [
+                    -80.66659927368164,
+                    35.04596652937105
+                ],
+                [
+                    -80.66640615463257,
+                    35.04561518428889
+                ],
+                [
+                    -80.6659984588623,
+                    35.045193568195565
+                ],
+                [
+                    -80.66552639007568,
+                    35.044877354697526
+                ],
+                [
+                    -80.6649899482727,
+                    35.04454357245502
+                ],
+                [
+                    -80.66449642181396,
+                    35.04417465365292
+                ],
+                [
+                    -80.66385269165039,
+                    35.04387600387859
+                ],
+                [
+                    -80.66303730010986,
+                    35.043717894732545
+                ]
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json
new file mode 100644
index 0000000..a7a82b8
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/linestring.json
@@ -0,0 +1,109 @@
+{
+    "type": "LineString",
+    "coordinates": [
+        [
+            -101.744384765625,
+            39.32155002466662
+        ],
+        [
+            -101.5521240234375,
+            39.330048552942415
+        ],
+        [
+            -101.40380859375,
+            39.330048552942415
+        ],
+        [
+            -101.33239746093749,
+            39.364032338047984
+        ],
+        [
+            -101.041259765625,
+            39.36827914916011
+        ],
+        [
+            -100.975341796875,
+            39.30454987014581
+        ],
+        [
+            -100.9149169921875,
+            39.24501680713314
+        ],
+        [
+            -100.843505859375,
+            39.16414104768742
+        ],
+        [
+            -100.8050537109375,
+            39.104488809440475
+        ],
+        [
+            -100.491943359375,
+            39.10022600175347
+        ],
+        [
+            -100.43701171875,
+            39.095962936305476
+        ],
+        [
+            -100.338134765625,
+            39.095962936305476
+        ],
+        [
+            -100.1953125,
+            39.027718840211605
+        ],
+        [
+            -100.008544921875,
+            39.01064750994083
+        ],
+        [
+            -99.86572265625,
+            39.00211029922512
+        ],
+        [
+            -99.6844482421875,
+            38.97222194853654
+        ],
+        [
+            -99.51416015625,
+            38.929502416386605
+        ],
+        [
+            -99.38232421875,
+            38.92095542046727
+        ],
+        [
+            -99.3218994140625,
+            38.89530825492018
+        ],
+        [
+            -99.1131591796875,
+            38.86965182408357
+        ],
+        [
+            -99.0802001953125,
+            38.85682013474361
+        ],
+        [
+            -98.82202148437499,
+            38.85682013474361
+        ],
+        [
+            -98.44848632812499,
+            38.84826438869913
+        ],
+        [
+            -98.20678710937499,
+            38.84826438869913
+        ],
+        [
+            -98.02001953125,
+            38.8782049970615
+        ],
+        [
+            -97.635498046875,
+            38.87392853923629
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json
new file mode 100644
index 0000000..40c662c
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/longValue.json
@@ -0,0 +1,24 @@
+
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Point",
+        "coordinates": [
+            -105.01621,
+            39.57422
+        ]
+      },
+      "properties": {
+        "size": 853555090789
+      }
+    }],
+  "crs": {
+    "type": "name",
+    "properties": {
+      "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json
new file mode 100644
index 0000000..8f23c20
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multilinestring.json
@@ -0,0 +1,85 @@
+{
+    "type": "MultiLineString",
+    "coordinates": [
+        [
+            [
+                -105.0214433670044,
+                39.57805759162015
+            ],
+            [
+                -105.02150774002075,
+                39.57780951131517
+            ],
+            [
+                -105.02157211303711,
+                39.57749527498758
+            ],
+            [
+                -105.02157211303711,
+                39.57716449836683
+            ],
+            [
+                -105.02157211303711,
+                39.57703218727656
+            ],
+            [
+                -105.02152919769287,
+                39.57678410330158
+            ]
+        ],
+        [
+            [
+                -105.01989841461182,
+                39.574997872470774
+            ],
+            [
+                -105.01959800720215,
+                39.57489863607502
+            ],
+            [
+                -105.01906156539916,
+                39.57478286010041
+            ]
+        ],
+        [
+            [
+                -105.01717329025269,
+                39.5744024519653
+            ],
+            [
+                -105.01698017120361,
+                39.574385912433804
+            ],
+            [
+                -105.0166368484497,
+                39.574385912433804
+            ],
+            [
+                -105.01650810241699,
+                39.5744024519653
+            ],
+            [
+                -105.0159502029419,
+                39.574270135602866
+            ]
+        ],
+        [
+            [
+                -105.0142765045166,
+                39.57397242286402
+            ],
+            [
+                -105.01412630081175,
+                39.57403858136094
+            ],
+            [
+                -105.0138258934021,
+                39.57417089816531
+            ],
+            [
+                -105.01331090927124,
+                39.57445207053608
+            ]
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json
new file mode 100644
index 0000000..63a7143
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipoint.json
@@ -0,0 +1,13 @@
+{
+    "type": "MultiPoint",
+    "coordinates": [
+        [
+            -105.01621,
+            39.57422
+        ],
+        [
+            -80.6665134,
+            35.0539943
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json
new file mode 100644
index 0000000..f25f85b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/multipolygon.json
@@ -0,0 +1,815 @@
+{
+    "type": "MultiPolygon",
+    "coordinates": [
+        [
+            [
+                [
+                    -84.32281494140625,
+                    34.9895035675793
+                ],
+                [
+                    -84.29122924804688,
+                    35.21981940793435
+                ],
+                [
+                    -84.24041748046875,
+                    35.25459097465022
+                ],
+                [
+                    -84.22531127929688,
+                    35.266925688950074
+                ],
+                [
+                    -84.20745849609375,
+                    35.26580442886754
+                ],
+                [
+                    -84.19921875,
+                    35.24674063355999
+                ],
+                [
+                    -84.16213989257812,
+                    35.24113278166642
+                ],
+                [
+                    -84.12368774414062,
+                    35.24898366572645
+                ],
+                [
+                    -84.09072875976562,
+                    35.24898366572645
+                ],
+                [
+                    -84.08798217773438,
+                    35.264683153268116
+                ],
+                [
+                    -84.04266357421875,
+                    35.27701633139884
+                ],
+                [
+                    -84.03030395507812,
+                    35.291589484566124
+                ],
+                [
+                    -84.0234375,
+                    35.306160014550784
+                ],
+                [
+                    -84.03305053710936,
+                    35.32745068492882
+                ],
+                [
+                    -84.03579711914062,
+                    35.34313496028189
+                ],
+                [
+                    -84.03579711914062,
+                    35.348735749472546
+                ],
+                [
+                    -84.01657104492188,
+                    35.35545618392078
+                ],
+                [
+                    -84.01107788085938,
+                    35.37337460834958
+                ],
+                [
+                    -84.00970458984374,
+                    35.39128905521763
+                ],
+                [
+                    -84.01931762695312,
+                    35.41479572901859
+                ],
+                [
+                    -84.00283813476562,
+                    35.429344044107154
+                ],
+                [
+                    -83.93692016601562,
+                    35.47409160773029
+                ],
+                [
+                    -83.91220092773438,
+                    35.47632833265728
+                ],
+                [
+                    -83.88885498046875,
+                    35.504282143299655
+                ],
+                [
+                    -83.88473510742186,
+                    35.516578738902936
+                ],
+                [
+                    -83.8751220703125,
+                    35.52104976129943
+                ],
+                [
+                    -83.85314941406249,
+                    35.52104976129943
+                ],
+                [
+                    -83.82843017578125,
+                    35.52104976129943
+                ],
+                [
+                    -83.8092041015625,
+                    35.53446133418443
+                ],
+                [
+                    -83.80233764648438,
+                    35.54116627999813
+                ],
+                [
+                    -83.76800537109374,
+                    35.56239491058853
+                ],
+                [
+                    -83.7432861328125,
+                    35.56239491058853
+                ],
+                [
+                    -83.71994018554688,
+                    35.56239491058853
+                ],
+                [
+                    -83.67050170898438,
+                    35.569097520776054
+                ],
+                [
+                    -83.6334228515625,
+                    35.570214567965984
+                ],
+                [
+                    -83.61007690429688,
+                    35.576916524038616
+                ],
+                [
+                    -83.59634399414061,
+                    35.574682600980914
+                ],
+                [
+                    -83.5894775390625,
+                    35.55904339525896
+                ],
+                [
+                    -83.55239868164062,
+                    35.56574628576276
+                ],
+                [
+                    -83.49746704101562,
+                    35.563512051219696
+                ],
+                [
+                    -83.47000122070312,
+                    35.586968406786475
+                ],
+                [
+                    -83.4466552734375,
+                    35.60818490437746
+                ],
+                [
+                    -83.37936401367188,
+                    35.63609277863135
+                ],
+                [
+                    -83.35739135742188,
+                    35.65618041632016
+                ],
+                [
+                    -83.32305908203124,
+                    35.66622234103479
+                ],
+                [
+                    -83.3148193359375,
+                    35.65394870599763
+                ],
+                [
+                    -83.29971313476561,
+                    35.660643649881614
+                ],
+                [
+                    -83.28598022460938,
+                    35.67180064238771
+                ],
+                [
+                    -83.26126098632811,
+                    35.6907639509368
+                ],
+                [
+                    -83.25714111328125,
+                    35.69968630125201
+                ],
+                [
+                    -83.25576782226562,
+                    35.715298012125295
+                ],
+                [
+                    -83.23516845703125,
+                    35.72310272092263
+                ],
+                [
+                    -83.19808959960936,
+                    35.72756221127198
+                ],
+                [
+                    -83.16238403320312,
+                    35.753199435570316
+                ],
+                [
+                    -83.15826416015625,
+                    35.76322914549896
+                ],
+                [
+                    -83.10333251953125,
+                    35.76991491635478
+                ],
+                [
+                    -83.08685302734375,
+                    35.7843988251953
+                ],
+                [
+                    -83.0511474609375,
+                    35.787740890986576
+                ],
+                [
+                    -83.01681518554688,
+                    35.78328477203738
+                ],
+                [
+                    -83.001708984375,
+                    35.77882840327371
+                ],
+                [
+                    -82.96737670898438,
+                    35.793310688351724
+                ],
+                [
+                    -82.94540405273438,
+                    35.820040281161
+                ],
+                [
+                    -82.9193115234375,
+                    35.85121343450061
+                ],
+                [
+                    -82.9083251953125,
+                    35.86902116501695
+                ],
+                [
+                    -82.90557861328125,
+                    35.87792352995116
+                ],
+                [
+                    -82.91244506835938,
+                    35.92353244718235
+                ],
+                [
+                    -82.88360595703125,
+                    35.94688293218141
+                ],
+                [
+                    -82.85614013671875,
+                    35.951329861522666
+                ],
+                [
+                    -82.8424072265625,
+                    35.94243575255426
+                ],
+                [
+                    -82.825927734375,
+                    35.92464453144099
+                ],
+                [
+                    -82.80670166015625,
+                    35.927980690382704
+                ],
+                [
+                    -82.80532836914062,
+                    35.94243575255426
+                ],
+                [
+                    -82.77923583984375,
+                    35.97356075349624
+                ],
+                [
+                    -82.78060913085938,
+                    35.99245209055831
+                ],
+                [
+                    -82.76138305664062,
+                    36.00356252895066
+                ],
+                [
+                    -82.69546508789062,
+                    36.04465753921525
+                ],
+                [
+                    -82.64465332031249,
+                    36.060201412392914
+                ],
+                [
+                    -82.61306762695312,
+                    36.060201412392914
+                ],
+                [
+                    -82.60620117187499,
+                    36.033552893400376
+                ],
+                [
+                    -82.60620117187499,
+                    35.991340960635405
+                ],
+                [
+                    -82.60620117187499,
+                    35.97911749857497
+                ],
+                [
+                    -82.5787353515625,
+                    35.96133453736691
+                ],
+                [
+                    -82.5677490234375,
+                    35.951329861522666
+                ],
+                [
+                    -82.53067016601562,
+                    35.97244935753683
+                ],
+                [
+                    -82.46475219726562,
+                    36.006895355244666
+                ],
+                [
+                    -82.41668701171875,
+                    36.070192281208456
+                ],
+                [
+                    -82.37960815429686,
+                    36.10126686921446
+                ],
+                [
+                    -82.35488891601562,
+                    36.117908916563685
+                ],
+                [
+                    -82.34115600585936,
+                    36.113471382052175
+                ],
+                [
+                    -82.29583740234375,
+                    36.13343831245866
+                ],
+                [
+                    -82.26287841796874,
+                    36.13565654678543
+                ],
+                [
+                    -82.23403930664062,
+                    36.13565654678543
+                ],
+                [
+                    -82.2216796875,
+                    36.154509006695
+                ],
+                [
+                    -82.20382690429688,
+                    36.15561783381855
+                ],
+                [
+                    -82.19009399414062,
+                    36.144528857027744
+                ],
+                [
+                    -82.15438842773438,
+                    36.15007354140755
+                ],
+                [
+                    -82.14065551757812,
+                    36.134547437460064
+                ],
+                [
+                    -82.1337890625,
+                    36.116799556445024
+                ],
+                [
+                    -82.12142944335938,
+                    36.10570509327921
+                ],
+                [
+                    -82.08984375,
+                    36.10792411128649
+                ],
+                [
+                    -82.05276489257811,
+                    36.12678323326429
+                ],
+                [
+                    -82.03628540039062,
+                    36.12900165569652
+                ],
+                [
+                    -81.91268920898438,
+                    36.29409768373033
+                ],
+                [
+                    -81.89071655273438,
+                    36.30959215409138
+                ],
+                [
+                    -81.86325073242188,
+                    36.33504067209607
+                ],
+                [
+                    -81.83029174804688,
+                    36.34499652561904
+                ],
+                [
+                    -81.80145263671875,
+                    36.35605709240176
+                ],
+                [
+                    -81.77947998046874,
+                    36.34610265300638
+                ],
+                [
+                    -81.76162719726562,
+                    36.33835943134047
+                ],
+                [
+                    -81.73690795898438,
+                    36.33835943134047
+                ],
+                [
+                    -81.71905517578125,
+                    36.33835943134047
+                ],
+                [
+                    -81.70669555664062,
+                    36.33504067209607
+                ],
+                [
+                    -81.70669555664062,
+                    36.342784223707234
+                ],
+                [
+                    -81.72317504882812,
+                    36.357163062654365
+                ],
+                [
+                    -81.73278808593749,
+                    36.379279167407965
+                ],
+                [
+                    -81.73690795898438,
+                    36.40028364332352
+                ],
+                [
+                    -81.73690795898438,
+                    36.41354670392876
+                ],
+                [
+                    -81.72454833984374,
+                    36.423492513472326
+                ],
+                [
+                    -81.71768188476562,
+                    36.445589751779174
+                ],
+                [
+                    -81.69845581054688,
+                    36.47541104282962
+                ],
+                [
+                    -81.69845581054688,
+                    36.51073994146672
+                ],
+                [
+                    -81.705322265625,
+                    36.53060536411363
+                ],
+                [
+                    -81.69158935546875,
+                    36.55929085774001
+                ],
+                [
+                    -81.68060302734375,
+                    36.56480607840351
+                ],
+                [
+                    -81.68197631835938,
+                    36.58686302344181
+                ],
+                [
+                    -81.04202270507812,
+                    36.56370306576917
+                ],
+                [
+                    -80.74264526367186,
+                    36.561496993252575
+                ],
+                [
+                    -79.89120483398438,
+                    36.54053616262899
+                ],
+                [
+                    -78.68408203124999,
+                    36.53943280355122
+                ],
+                [
+                    -77.88345336914062,
+                    36.54053616262899
+                ],
+                [
+                    -76.91665649414062,
+                    36.54163950596125
+                ],
+                [
+                    -76.91665649414062,
+                    36.55046568575947
+                ],
+                [
+                    -76.31103515625,
+                    36.551568887374
+                ],
+                [
+                    -75.79605102539062,
+                    36.54936246839778
+                ],
+                [
+                    -75.6298828125,
+                    36.07574221562703
+                ],
+                [
+                    -75.4925537109375,
+                    35.82226734114509
+                ],
+                [
+                    -75.3936767578125,
+                    35.639441068973916
+                ],
+                [
+                    -75.41015624999999,
+                    35.43829554739668
+                ],
+                [
+                    -75.43212890625,
+                    35.263561862152095
+                ],
+                [
+                    -75.487060546875,
+                    35.18727767598896
+                ],
+                [
+                    -75.5914306640625,
+                    35.17380831799959
+                ],
+                [
+                    -75.9210205078125,
+                    35.04798673426734
+                ],
+                [
+                    -76.17919921875,
+                    34.867904962568744
+                ],
+                [
+                    -76.41540527343749,
+                    34.62868797377061
+                ],
+                [
+                    -76.4593505859375,
+                    34.57442951865274
+                ],
+                [
+                    -76.53076171875,
+                    34.53371242139567
+                ],
+                [
+                    -76.5911865234375,
+                    34.551811369170494
+                ],
+                [
+                    -76.651611328125,
+                    34.615126683462194
+                ],
+                [
+                    -76.761474609375,
+                    34.63320791137959
+                ],
+                [
+                    -77.069091796875,
+                    34.59704151614417
+                ],
+                [
+                    -77.376708984375,
+                    34.45674800347809
+                ],
+                [
+                    -77.5909423828125,
+                    34.3207552752374
+                ],
+                [
+                    -77.8326416015625,
+                    33.97980872872457
+                ],
+                [
+                    -77.9150390625,
+                    33.80197351806589
+                ],
+                [
+                    -77.9754638671875,
+                    33.73804486328907
+                ],
+                [
+                    -78.11279296875,
+                    33.8521697014074
+                ],
+                [
+                    -78.2830810546875,
+                    33.8521697014074
+                ],
+                [
+                    -78.4808349609375,
+                    33.815666308702774
+                ],
+                [
+                    -79.6728515625,
+                    34.8047829195724
+                ],
+                [
+                    -80.782470703125,
+                    34.836349990763864
+                ],
+                [
+                    -80.782470703125,
+                    34.91746688928252
+                ],
+                [
+                    -80.9307861328125,
+                    35.092945313732635
+                ],
+                [
+                    -81.0516357421875,
+                    35.02999636902566
+                ],
+                [
+                    -81.0516357421875,
+                    35.05248370662468
+                ],
+                [
+                    -81.0516357421875,
+                    35.137879119634185
+                ],
+                [
+                    -82.3150634765625,
+                    35.19625600786368
+                ],
+                [
+                    -82.3590087890625,
+                    35.19625600786368
+                ],
+                [
+                    -82.40295410156249,
+                    35.22318504970181
+                ],
+                [
+                    -82.4688720703125,
+                    35.16931803601131
+                ],
+                [
+                    -82.6885986328125,
+                    35.1154153142536
+                ],
+                [
+                    -82.781982421875,
+                    35.06147690849717
+                ],
+                [
+                    -83.1060791015625,
+                    35.003003395276714
+                ],
+                [
+                    -83.616943359375,
+                    34.99850370014629
+                ],
+                [
+                    -84.05639648437499,
+                    34.985003130171066
+                ],
+                [
+                    -84.22119140625,
+                    34.985003130171066
+                ],
+                [
+                    -84.32281494140625,
+                    34.9895035675793
+                ]
+            ],
+            [
+                [
+                    -75.69030761718749,
+                    35.74205383068037
+                ],
+                [
+                    -75.5914306640625,
+                    35.74205383068037
+                ],
+                [
+                    -75.5419921875,
+                    35.585851593232356
+                ],
+                [
+                    -75.56396484375,
+                    35.32633026307483
+                ],
+                [
+                    -75.69030761718749,
+                    35.285984736065735
+                ],
+                [
+                    -75.970458984375,
+                    35.16482750605027
+                ],
+                [
+                    -76.2066650390625,
+                    34.994003757575776
+                ],
+                [
+                    -76.300048828125,
+                    35.02999636902566
+                ],
+                [
+                    -76.409912109375,
+                    35.07946034047981
+                ],
+                [
+                    -76.5252685546875,
+                    35.10642805736423
+                ],
+                [
+                    -76.4208984375,
+                    35.25907654252574
+                ],
+                [
+                    -76.3385009765625,
+                    35.294952147406576
+                ],
+                [
+                    -76.0858154296875,
+                    35.29943548054543
+                ],
+                [
+                    -75.948486328125,
+                    35.44277092585766
+                ],
+                [
+                    -75.8660888671875,
+                    35.53669637839501
+                ],
+                [
+                    -75.772705078125,
+                    35.567980458012094
+                ],
+                [
+                    -75.706787109375,
+                    35.634976650677295
+                ],
+                [
+                    -75.706787109375,
+                    35.74205383068037
+                ],
+                [
+                    -75.69030761718749,
+                    35.74205383068037
+                ]
+            ]
+        ],
+        [
+            [
+                [
+                    -109.0283203125,
+                    36.98500309285596
+                ],
+                [
+                    -109.0283203125,
+                    40.97989806962013
+                ],
+                [
+                    -102.06298828125,
+                    40.97989806962013
+                ],
+                [
+                    -102.06298828125,
+                    37.00255267215955
+                ],
+                [
+                    -109.0283203125,
+                    36.98500309285596
+                ]
+            ]
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json
new file mode 100644
index 0000000..9b8d23b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/point.json
@@ -0,0 +1,7 @@
+{
+    "type": "Point",
+    "coordinates": [
+        -105.01621,
+        39.57422
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json
new file mode 100644
index 0000000..77bc6fc
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/polygon.json
@@ -0,0 +1,789 @@
+{
+    "type": "Polygon",
+    "coordinates": [
+        [
+            [
+                -84.32281494140625,
+                34.9895035675793
+            ],
+            [
+                -84.29122924804688,
+                35.21981940793435
+            ],
+            [
+                -84.24041748046875,
+                35.25459097465022
+            ],
+            [
+                -84.22531127929688,
+                35.266925688950074
+            ],
+            [
+                -84.20745849609375,
+                35.26580442886754
+            ],
+            [
+                -84.19921875,
+                35.24674063355999
+            ],
+            [
+                -84.16213989257812,
+                35.24113278166642
+            ],
+            [
+                -84.12368774414062,
+                35.24898366572645
+            ],
+            [
+                -84.09072875976562,
+                35.24898366572645
+            ],
+            [
+                -84.08798217773438,
+                35.264683153268116
+            ],
+            [
+                -84.04266357421875,
+                35.27701633139884
+            ],
+            [
+                -84.03030395507812,
+                35.291589484566124
+            ],
+            [
+                -84.0234375,
+                35.306160014550784
+            ],
+            [
+                -84.03305053710936,
+                35.32745068492882
+            ],
+            [
+                -84.03579711914062,
+                35.34313496028189
+            ],
+            [
+                -84.03579711914062,
+                35.348735749472546
+            ],
+            [
+                -84.01657104492188,
+                35.35545618392078
+            ],
+            [
+                -84.01107788085938,
+                35.37337460834958
+            ],
+            [
+                -84.00970458984374,
+                35.39128905521763
+            ],
+            [
+                -84.01931762695312,
+                35.41479572901859
+            ],
+            [
+                -84.00283813476562,
+                35.429344044107154
+            ],
+            [
+                -83.93692016601562,
+                35.47409160773029
+            ],
+            [
+                -83.91220092773438,
+                35.47632833265728
+            ],
+            [
+                -83.88885498046875,
+                35.504282143299655
+            ],
+            [
+                -83.88473510742186,
+                35.516578738902936
+            ],
+            [
+                -83.8751220703125,
+                35.52104976129943
+            ],
+            [
+                -83.85314941406249,
+                35.52104976129943
+            ],
+            [
+                -83.82843017578125,
+                35.52104976129943
+            ],
+            [
+                -83.8092041015625,
+                35.53446133418443
+            ],
+            [
+                -83.80233764648438,
+                35.54116627999813
+            ],
+            [
+                -83.76800537109374,
+                35.56239491058853
+            ],
+            [
+                -83.7432861328125,
+                35.56239491058853
+            ],
+            [
+                -83.71994018554688,
+                35.56239491058853
+            ],
+            [
+                -83.67050170898438,
+                35.569097520776054
+            ],
+            [
+                -83.6334228515625,
+                35.570214567965984
+            ],
+            [
+                -83.61007690429688,
+                35.576916524038616
+            ],
+            [
+                -83.59634399414061,
+                35.574682600980914
+            ],
+            [
+                -83.5894775390625,
+                35.55904339525896
+            ],
+            [
+                -83.55239868164062,
+                35.56574628576276
+            ],
+            [
+                -83.49746704101562,
+                35.563512051219696
+            ],
+            [
+                -83.47000122070312,
+                35.586968406786475
+            ],
+            [
+                -83.4466552734375,
+                35.60818490437746
+            ],
+            [
+                -83.37936401367188,
+                35.63609277863135
+            ],
+            [
+                -83.35739135742188,
+                35.65618041632016
+            ],
+            [
+                -83.32305908203124,
+                35.66622234103479
+            ],
+            [
+                -83.3148193359375,
+                35.65394870599763
+            ],
+            [
+                -83.29971313476561,
+                35.660643649881614
+            ],
+            [
+                -83.28598022460938,
+                35.67180064238771
+            ],
+            [
+                -83.26126098632811,
+                35.6907639509368
+            ],
+            [
+                -83.25714111328125,
+                35.69968630125201
+            ],
+            [
+                -83.25576782226562,
+                35.715298012125295
+            ],
+            [
+                -83.23516845703125,
+                35.72310272092263
+            ],
+            [
+                -83.19808959960936,
+                35.72756221127198
+            ],
+            [
+                -83.16238403320312,
+                35.753199435570316
+            ],
+            [
+                -83.15826416015625,
+                35.76322914549896
+            ],
+            [
+                -83.10333251953125,
+                35.76991491635478
+            ],
+            [
+                -83.08685302734375,
+                35.7843988251953
+            ],
+            [
+                -83.0511474609375,
+                35.787740890986576
+            ],
+            [
+                -83.01681518554688,
+                35.78328477203738
+            ],
+            [
+                -83.001708984375,
+                35.77882840327371
+            ],
+            [
+                -82.96737670898438,
+                35.793310688351724
+            ],
+            [
+                -82.94540405273438,
+                35.820040281161
+            ],
+            [
+                -82.9193115234375,
+                35.85121343450061
+            ],
+            [
+                -82.9083251953125,
+                35.86902116501695
+            ],
+            [
+                -82.90557861328125,
+                35.87792352995116
+            ],
+            [
+                -82.91244506835938,
+                35.92353244718235
+            ],
+            [
+                -82.88360595703125,
+                35.94688293218141
+            ],
+            [
+                -82.85614013671875,
+                35.951329861522666
+            ],
+            [
+                -82.8424072265625,
+                35.94243575255426
+            ],
+            [
+                -82.825927734375,
+                35.92464453144099
+            ],
+            [
+                -82.80670166015625,
+                35.927980690382704
+            ],
+            [
+                -82.80532836914062,
+                35.94243575255426
+            ],
+            [
+                -82.77923583984375,
+                35.97356075349624
+            ],
+            [
+                -82.78060913085938,
+                35.99245209055831
+            ],
+            [
+                -82.76138305664062,
+                36.00356252895066
+            ],
+            [
+                -82.69546508789062,
+                36.04465753921525
+            ],
+            [
+                -82.64465332031249,
+                36.060201412392914
+            ],
+            [
+                -82.61306762695312,
+                36.060201412392914
+            ],
+            [
+                -82.60620117187499,
+                36.033552893400376
+            ],
+            [
+                -82.60620117187499,
+                35.991340960635405
+            ],
+            [
+                -82.60620117187499,
+                35.97911749857497
+            ],
+            [
+                -82.5787353515625,
+                35.96133453736691
+            ],
+            [
+                -82.5677490234375,
+                35.951329861522666
+            ],
+            [
+                -82.53067016601562,
+                35.97244935753683
+            ],
+            [
+                -82.46475219726562,
+                36.006895355244666
+            ],
+            [
+                -82.41668701171875,
+                36.070192281208456
+            ],
+            [
+                -82.37960815429686,
+                36.10126686921446
+            ],
+            [
+                -82.35488891601562,
+                36.117908916563685
+            ],
+            [
+                -82.34115600585936,
+                36.113471382052175
+            ],
+            [
+                -82.29583740234375,
+                36.13343831245866
+            ],
+            [
+                -82.26287841796874,
+                36.13565654678543
+            ],
+            [
+                -82.23403930664062,
+                36.13565654678543
+            ],
+            [
+                -82.2216796875,
+                36.154509006695
+            ],
+            [
+                -82.20382690429688,
+                36.15561783381855
+            ],
+            [
+                -82.19009399414062,
+                36.144528857027744
+            ],
+            [
+                -82.15438842773438,
+                36.15007354140755
+            ],
+            [
+                -82.14065551757812,
+                36.134547437460064
+            ],
+            [
+                -82.1337890625,
+                36.116799556445024
+            ],
+            [
+                -82.12142944335938,
+                36.10570509327921
+            ],
+            [
+                -82.08984375,
+                36.10792411128649
+            ],
+            [
+                -82.05276489257811,
+                36.12678323326429
+            ],
+            [
+                -82.03628540039062,
+                36.12900165569652
+            ],
+            [
+                -81.91268920898438,
+                36.29409768373033
+            ],
+            [
+                -81.89071655273438,
+                36.30959215409138
+            ],
+            [
+                -81.86325073242188,
+                36.33504067209607
+            ],
+            [
+                -81.83029174804688,
+                36.34499652561904
+            ],
+            [
+                -81.80145263671875,
+                36.35605709240176
+            ],
+            [
+                -81.77947998046874,
+                36.34610265300638
+            ],
+            [
+                -81.76162719726562,
+                36.33835943134047
+            ],
+            [
+                -81.73690795898438,
+                36.33835943134047
+            ],
+            [
+                -81.71905517578125,
+                36.33835943134047
+            ],
+            [
+                -81.70669555664062,
+                36.33504067209607
+            ],
+            [
+                -81.70669555664062,
+                36.342784223707234
+            ],
+            [
+                -81.72317504882812,
+                36.357163062654365
+            ],
+            [
+                -81.73278808593749,
+                36.379279167407965
+            ],
+            [
+                -81.73690795898438,
+                36.40028364332352
+            ],
+            [
+                -81.73690795898438,
+                36.41354670392876
+            ],
+            [
+                -81.72454833984374,
+                36.423492513472326
+            ],
+            [
+                -81.71768188476562,
+                36.445589751779174
+            ],
+            [
+                -81.69845581054688,
+                36.47541104282962
+            ],
+            [
+                -81.69845581054688,
+                36.51073994146672
+            ],
+            [
+                -81.705322265625,
+                36.53060536411363
+            ],
+            [
+                -81.69158935546875,
+                36.55929085774001
+            ],
+            [
+                -81.68060302734375,
+                36.56480607840351
+            ],
+            [
+                -81.68197631835938,
+                36.58686302344181
+            ],
+            [
+                -81.04202270507812,
+                36.56370306576917
+            ],
+            [
+                -80.74264526367186,
+                36.561496993252575
+            ],
+            [
+                -79.89120483398438,
+                36.54053616262899
+            ],
+            [
+                -78.68408203124999,
+                36.53943280355122
+            ],
+            [
+                -77.88345336914062,
+                36.54053616262899
+            ],
+            [
+                -76.91665649414062,
+                36.54163950596125
+            ],
+            [
+                -76.91665649414062,
+                36.55046568575947
+            ],
+            [
+                -76.31103515625,
+                36.551568887374
+            ],
+            [
+                -75.79605102539062,
+                36.54936246839778
+            ],
+            [
+                -75.6298828125,
+                36.07574221562703
+            ],
+            [
+                -75.4925537109375,
+                35.82226734114509
+            ],
+            [
+                -75.3936767578125,
+                35.639441068973916
+            ],
+            [
+                -75.41015624999999,
+                35.43829554739668
+            ],
+            [
+                -75.43212890625,
+                35.263561862152095
+            ],
+            [
+                -75.487060546875,
+                35.18727767598896
+            ],
+            [
+                -75.5914306640625,
+                35.17380831799959
+            ],
+            [
+                -75.9210205078125,
+                35.04798673426734
+            ],
+            [
+                -76.17919921875,
+                34.867904962568744
+            ],
+            [
+                -76.41540527343749,
+                34.62868797377061
+            ],
+            [
+                -76.4593505859375,
+                34.57442951865274
+            ],
+            [
+                -76.53076171875,
+                34.53371242139567
+            ],
+            [
+                -76.5911865234375,
+                34.551811369170494
+            ],
+            [
+                -76.651611328125,
+                34.615126683462194
+            ],
+            [
+                -76.761474609375,
+                34.63320791137959
+            ],
+            [
+                -77.069091796875,
+                34.59704151614417
+            ],
+            [
+                -77.376708984375,
+                34.45674800347809
+            ],
+            [
+                -77.5909423828125,
+                34.3207552752374
+            ],
+            [
+                -77.8326416015625,
+                33.97980872872457
+            ],
+            [
+                -77.9150390625,
+                33.80197351806589
+            ],
+            [
+                -77.9754638671875,
+                33.73804486328907
+            ],
+            [
+                -78.11279296875,
+                33.8521697014074
+            ],
+            [
+                -78.2830810546875,
+                33.8521697014074
+            ],
+            [
+                -78.4808349609375,
+                33.815666308702774
+            ],
+            [
+                -79.6728515625,
+                34.8047829195724
+            ],
+            [
+                -80.782470703125,
+                34.836349990763864
+            ],
+            [
+                -80.782470703125,
+                34.91746688928252
+            ],
+            [
+                -80.9307861328125,
+                35.092945313732635
+            ],
+            [
+                -81.0516357421875,
+                35.02999636902566
+            ],
+            [
+                -81.0516357421875,
+                35.05248370662468
+            ],
+            [
+                -81.0516357421875,
+                35.137879119634185
+            ],
+            [
+                -82.3150634765625,
+                35.19625600786368
+            ],
+            [
+                -82.3590087890625,
+                35.19625600786368
+            ],
+            [
+                -82.40295410156249,
+                35.22318504970181
+            ],
+            [
+                -82.4688720703125,
+                35.16931803601131
+            ],
+            [
+                -82.6885986328125,
+                35.1154153142536
+            ],
+            [
+                -82.781982421875,
+                35.06147690849717
+            ],
+            [
+                -83.1060791015625,
+                35.003003395276714
+            ],
+            [
+                -83.616943359375,
+                34.99850370014629
+            ],
+            [
+                -84.05639648437499,
+                34.985003130171066
+            ],
+            [
+                -84.22119140625,
+                34.985003130171066
+            ],
+            [
+                -84.32281494140625,
+                34.9895035675793
+            ]
+        ],
+        [
+            [
+                -75.69030761718749,
+                35.74205383068037
+            ],
+            [
+                -75.5914306640625,
+                35.74205383068037
+            ],
+            [
+                -75.5419921875,
+                35.585851593232356
+            ],
+            [
+                -75.56396484375,
+                35.32633026307483
+            ],
+            [
+                -75.69030761718749,
+                35.285984736065735
+            ],
+            [
+                -75.970458984375,
+                35.16482750605027
+            ],
+            [
+                -76.2066650390625,
+                34.994003757575776
+            ],
+            [
+                -76.300048828125,
+                35.02999636902566
+            ],
+            [
+                -76.409912109375,
+                35.07946034047981
+            ],
+            [
+                -76.5252685546875,
+                35.10642805736423
+            ],
+            [
+                -76.4208984375,
+                35.25907654252574
+            ],
+            [
+                -76.3385009765625,
+                35.294952147406576
+            ],
+            [
+                -76.0858154296875,
+                35.29943548054543
+            ],
+            [
+                -75.948486328125,
+                35.44277092585766
+            ],
+            [
+                -75.8660888671875,
+                35.53669637839501
+            ],
+            [
+                -75.772705078125,
+                35.567980458012094
+            ],
+            [
+                -75.706787109375,
+                35.634976650677295
+            ],
+            [
+                -75.706787109375,
+                35.74205383068037
+            ],
+            [
+                -75.69030761718749,
+                35.74205383068037
+            ]
+        ]
+    ]
+}
\ No newline at end of file
diff --git a/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json
new file mode 100644
index 0000000..720099b
--- /dev/null
+++ b/storage/sis-geojson/src/test/resources/org/apache/sis/internal/storage/geojson/sample_with_null_properties.json
@@ -0,0 +1,502 @@
+{
+  "type": "FeatureCollection",
+  "features": [
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.834892363946428,
+              43.48256740738211
+            ],
+            [
+              3.8171876558029396,
+              43.47857177599835
+            ],
+            [
+              3.6412946044113017,
+              43.473329643558024
+            ],
+            [
+              3.6589021586871184,
+              43.52508450712277
+            ],
+            [
+              3.8279642042032593,
+              43.52668001576773
+            ],
+            [
+              3.834892363946428,
+              43.48256740738211
+            ]
+          ]
+        ]
+      },
+      "properties": null
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.6946333003797647,
+              43.454643754862055
+            ],
+            [
+              3.7292860446964538,
+              43.40531143854098
+            ],
+            [
+              3.636426622539704,
+              43.43205503941921
+            ],
+            [
+              3.6946333003797647,
+              43.454643754862055
+            ]
+          ]
+        ]
+      },
+      "properties": null
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823533
+            ],
+            [
+              3.6300962842853837,
+              43.371516200704754
+            ],
+            [
+              3.5565671428569434,
+              43.31364096674475
+            ],
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823533
+            ],
+            [
+              3.6300962842853837,
+              43.371516200704754
+            ],
+            [
+              3.5565671428569434,
+              43.31364096674475
+            ],
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784403,
+              43.31621752144202
+            ],
+            [
+              3.6238508748053055,
+              43.375682458235445
+            ],
+            [
+              3.6300962842853837,
+              43.37151620070485
+            ],
+            [
+              3.556567142856943,
+              43.31364096674483
+            ],
+            [
+              3.5462226673784403,
+              43.31621752144202
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.633385150301334,
+              43.38457565109916
+            ],
+            [
+              3.64118828280889,
+              43.37902289968621
+            ],
+            [
+              3.654474857633081,
+              43.38559747028079
+            ],
+            [
+              3.6479331503130545,
+              43.391373120329625
+            ],
+            [
+              3.633385150301334,
+              43.38457565109916
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Parking visiteurs"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784416,
+              43.31621752144171
+            ],
+            [
+              3.6238508748053064,
+              43.37568245823512
+            ],
+            [
+              3.6300962842853846,
+              43.371516200704534
+            ],
+            [
+              3.5565671428569443,
+              43.31364096674452
+            ],
+            [
+              3.5462226673784416,
+              43.31621752144171
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784416,
+              43.31621752144171
+            ],
+            [
+              3.6238508748053064,
+              43.37568245823512
+            ],
+            [
+              3.6300962842853846,
+              43.371516200704534
+            ],
+            [
+              3.5565671428569443,
+              43.31364096674452
+            ],
+            [
+              3.5462226673784416,
+              43.31621752144171
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823533
+            ],
+            [
+              3.6300962842853837,
+              43.371516200704754
+            ],
+            [
+              3.5565671428569434,
+              43.31364096674475
+            ],
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823533
+            ],
+            [
+              3.6300962842853837,
+              43.371516200704754
+            ],
+            [
+              3.5565671428569434,
+              43.31364096674475
+            ],
+            [
+              3.5462226673784407,
+              43.31621752144193
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    },
+    {
+      "type": "Feature",
+      "geometry": {
+        "type": "Polygon",
+        "coordinates": [
+          [
+            [
+              3.546222667378441,
+              43.31621752144182
+            ],
+            [
+              3.6238508748053055,
+              43.37568245823523
+            ],
+            [
+              3.6300962842853846,
+              43.37151620070465
+            ],
+            [
+              3.556567142856944,
+              43.31364096674463
+            ],
+            [
+              3.546222667378441,
+              43.31621752144182
+            ]
+          ]
+        ]
+      },
+      "properties": {
+        "label": "Marathon"
+      }
+    }
+  ],
+  "crs": {
+    "type": "name",
+    "properties": {
+      "name": "urn:ogc:def:crs:OGC:1.3:CRS84"
+    }
+  }
+}