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 2023/05/12 15:09:25 UTC

[sis] 01/01: feat(coveragejson): initial binding and grid coverage implementation of CoverageJson

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

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

commit 48c170932fed91e60c6c7d02529dbd72dd5b00f5
Author: jsorel <jo...@geomatys.com>
AuthorDate: Fri May 12 17:08:22 2023 +0200

    feat(coveragejson): initial binding and grid coverage implementation of CoverageJson
---
 storage/pom.xml                                    |   1 +
 storage/{ => sis-coveragejson}/pom.xml             | 153 +++----
 .../internal/coveragejson/CoverageJsonStore.java   | 128 ++++++
 .../coveragejson/CoverageJsonStoreProvider.java    |  99 +++++
 .../internal/coveragejson/CoverageResource.java    | 457 +++++++++++++++++++++
 .../sis/internal/coveragejson/binding/Axe.java     | 136 ++++++
 .../sis/internal/coveragejson/binding/Axes.java    |  58 +++
 .../internal/coveragejson/binding/Category.java    |  68 +++
 .../coveragejson/binding/CategoryEncoding.java     |  31 ++
 .../internal/coveragejson/binding/Coverage.java    | 131 ++++++
 .../coveragejson/binding/CoverageCollection.java   |  90 ++++
 .../coveragejson/binding/CoverageJsonObject.java   |  55 +++
 .../internal/coveragejson/binding/Dictionary.java  |  69 ++++
 .../sis/internal/coveragejson/binding/Domain.java  |  90 ++++
 .../coveragejson/binding/GeographicCRS.java        |  72 ++++
 .../sis/internal/coveragejson/binding/I18N.java    | 119 ++++++
 .../coveragejson/binding/IdentifierRS.java         |  84 ++++
 .../internal/coveragejson/binding/Identifiers.java |  29 ++
 .../sis/internal/coveragejson/binding/NdArray.java | 104 +++++
 .../coveragejson/binding/ObservedProperty.java     |  88 ++++
 .../internal/coveragejson/binding/Parameter.java   |  91 ++++
 .../coveragejson/binding/ParameterGroup.java       |  86 ++++
 .../internal/coveragejson/binding/Parameters.java  |  71 ++++
 .../coveragejson/binding/ProjectedCRS.java         |  64 +++
 .../sis/internal/coveragejson/binding/Ranges.java  |  67 +++
 .../binding/ReferenceSystemConnection.java         |  67 +++
 .../sis/internal/coveragejson/binding/Symbol.java  |  60 +++
 .../coveragejson/binding/TargetConcept.java        |  54 +++
 .../internal/coveragejson/binding/TemporalRS.java  |  79 ++++
 .../sis/internal/coveragejson/binding/TileSet.java |  75 ++++
 .../coveragejson/binding/TiledNdArray.java         |  79 ++++
 .../sis/internal/coveragejson/binding/Unit.java    |  79 ++++
 .../internal/coveragejson/binding/VerticalCRS.java |  61 +++
 .../org.apache.sis.storage.DataStoreProvider       |   1 +
 .../coveragejson/CoverageJsonStoreTest.java        |  86 ++++
 .../coveragejson/CoverageJsonTestSuite.java        |  44 ++
 .../internal/coveragejson/binding/BindingTest.java | 234 +++++++++++
 .../internal/coveragejson/binding/axe_bounds.json  |  12 +
 .../internal/coveragejson/binding/axe_polygon.json |  33 ++
 .../internal/coveragejson/binding/axe_regular.json |   5 +
 .../internal/coveragejson/binding/axe_tuples.json  |  20 +
 .../binding/coverage_vertical_profile.json         |  91 ++++
 .../binding/coverage_vertical_profile_nocs.json    |  80 ++++
 .../coveragejson/binding/coveragecollection.json   |  92 +++++
 .../internal/coveragejson/binding/domain_grid.json |  26 ++
 .../coveragejson/binding/domain_trajectory.json    |  27 ++
 .../coveragejson/binding/domaintype_grid.json      |   5 +
 .../binding/domaintype_multipoint.json             |  30 ++
 .../binding/domaintype_multipointseries.json       |  31 ++
 .../binding/domaintype_multipolygon.json           |  32 ++
 .../binding/domaintype_multipolygonseries.json     |  32 ++
 .../coveragejson/binding/domaintype_point.json     |  24 ++
 .../binding/domaintype_pointseries.json            |  26 ++
 .../coveragejson/binding/domaintype_polygon.json   |  29 ++
 .../binding/domaintype_polygonseries.json          |  31 ++
 .../coveragejson/binding/domaintype_section.json   |  31 ++
 .../binding/domaintype_trajectory.json             |  30 ++
 .../binding/domaintype_vertical_profile.json       |  26 ++
 .../binding/geographiccrs_longlat.json             |   4 +
 .../binding/geographiccrs_longlatheight.json       |   4 +
 .../sis/internal/coveragejson/binding/ndarray.json |  10 +
 .../binding/parameter_categoricaldata.json         |  33 ++
 .../binding/parameter_continuousdata.json          |  24 ++
 .../binding/parametergroup_uncertainty.json        |  13 +
 .../binding/parametergroup_vectorquantity.json     |   9 +
 .../binding/projectedcrs_britishnationalgrid.json  |   4 +
 .../binding/reference_system_connection.json       |   7 +
 .../internal/coveragejson/binding/temporalrs.json  |   4 +
 .../coveragejson/binding/tiledndarray.json         |  16 +
 .../coveragejson/binding/verticalcrs_navd88.json   |   4 +
 .../sis/internal/coveragejson/coverage_xyzt.json   |  58 +++
 71 files changed, 4177 insertions(+), 86 deletions(-)

diff --git a/storage/pom.xml b/storage/pom.xml
index 4287379b06..b44712ecf0 100644
--- a/storage/pom.xml
+++ b/storage/pom.xml
@@ -175,6 +175,7 @@
     <module>sis-netcdf</module>
     <module>sis-geotiff</module>
     <module>sis-earth-observation</module>
+    <module>sis-coveragejson</module>
   </modules>
 
 </project>
diff --git a/storage/pom.xml b/storage/sis-coveragejson/pom.xml
similarity index 51%
copy from storage/pom.xml
copy to storage/sis-coveragejson/pom.xml
index 4287379b06..7ae7a10ba8 100644
--- a/storage/pom.xml
+++ b/storage/sis-coveragejson/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-coveragejson</artifactId>
+  <name>Apache SIS Coverage-JSON storage</name>
   <description>
-    Group of modules for reading and writing data from/to various storages.
-    Storages are typically file formats or a database schemas.
+    DataStore for Coverage-JSON format.
   </description>
 
 
@@ -59,40 +58,7 @@
         <role>developer</role>
       </roles>
     </developer>
-    <developer>
-      <name>Martin Desruisseaux</name>
-      <id>desruisseaux</id>
-      <email>desruisseaux@apache.org</email>
-      <organization>Geomatys</organization>
-      <organizationUrl>https://www.geomatys.com</organizationUrl>
-      <timezone>+1</timezone>
-      <roles>
-        <role>developer</role>
-      </roles>
-    </developer>
   </developers>
-  <contributors>
-    <contributor>
-      <name>Thi Phuong Hao Nguyen</name>
-      <email>nguyenthiphuonghao243@gmail.com</email>
-      <organization>VNSC</organization>
-      <organizationUrl>https://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>https://vnsc.org.vn</organizationUrl>
-      <timezone>+7</timezone>
-      <roles>
-        <role>developer</role>
-      </roles>
-    </contributor>
-  </contributors>
 
 
   <!-- ===========================================================
@@ -100,21 +66,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.coveragejson
+              </Automatic-Module-Name>
+            </manifestEntries>
+          </archive>
+        </configuration>
       </plugin>
     </plugins>
   </build>
@@ -125,56 +88,74 @@
        =========================================================== -->
   <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.storage</groupId>
+      <artifactId>sis-storage</artifactId>
       <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.apache.sis.core</groupId>
       <artifactId>sis-referencing</artifactId>
       <version>${project.version}</version>
+      <type>test-jar</type>
+      <scope>test</scope>
     </dependency>
+    
+    <!--dependency>
+        <groupId>jakarta.json</groupId>
+        <artifactId>jakarta.json-api</artifactId>
+        <version>2.1.1</version>
+    </dependency>
+    <dependency>
+        <groupId>jakarta.json.bind</groupId>
+        <artifactId>jakarta.json.bind-api</artifactId>
+        <version>3.0.0</version>
+    </dependency-->
+    
+    <!-- this dependencis pulls jakarta jsonb-api -->
     <dependency>
-      <groupId>org.opengis</groupId>
-      <artifactId>geoapi-pending</artifactId>
+        <groupId>org.eclipse</groupId>
+        <artifactId>yasson</artifactId>
+        <version>3.0.3</version>
     </dependency>
+    
+    <!--dependency>
+        <groupId>javax.json.bind</groupId>
+        <artifactId>javax.json.bind-api</artifactId>
+        <version>1.0</version>
+    </dependency--> 
+    <!--dependency>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-core</artifactId>
+        <version>1.2.19</version>
+    </dependency>    
 
-    <!-- Test dependencies -->
     <dependency>
-      <groupId>org.opengis</groupId>
-      <artifactId>geoapi-conformance</artifactId>
+      <groupId>org.apache.geronimo.specs</groupId>
+      <artifactId>geronimo-json_1.1_spec</artifactId>
+      <version>1.5</version>
     </dependency>
     <dependency>
-      <groupId>org.apache.derby</groupId>
-      <artifactId>derby</artifactId>
-      <scope>test</scope>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-mapper</artifactId>
+        <version>1.2.19</version>
     </dependency>
     <dependency>
-      <groupId>org.apache.derby</groupId>
-      <artifactId>derbytools</artifactId>
-      <scope>test</scope>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-jsonb</artifactId>
+        <version>1.2.19</version>
     </dependency>
     <dependency>
-      <groupId>org.apache.sis.core</groupId>
-      <artifactId>sis-utility</artifactId>
-      <version>${project.version}</version>
-      <type>test-jar</type>
-      <scope>test</scope>
-    </dependency>
+        <groupId>org.apache.johnzon</groupId>
+        <artifactId>johnzon-jsonb-extras</artifactId>
+        <version>1.2.19</version>
+    </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>
-  </modules>
-
 </project>
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java
new file mode 100644
index 0000000000..ac1800821f
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStore.java
@@ -0,0 +1,128 @@
+/*
+ * 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.coveragejson;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import org.apache.sis.internal.coveragejson.binding.Coverage;
+import org.apache.sis.internal.coveragejson.binding.CoverageCollection;
+import org.apache.sis.internal.coveragejson.binding.CoverageJsonObject;
+import org.apache.sis.internal.storage.MetadataBuilder;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.WritableAggregate;
+import org.opengis.metadata.Metadata;
+import org.opengis.parameter.ParameterValueGroup;
+
+/**
+ * A data store backed by Coverage-JSON files.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class CoverageJsonStore extends DataStore implements WritableAggregate {
+
+    /**
+     * The {@link CoverageJsonStoreProvider#LOCATION} parameter value, or {@code null} if none.
+     * This is used for information purpose only, not for actual reading operations.
+     *
+     * @see #getOpenParameters()
+     */
+    private final URI location;
+
+    /**
+     * Same value than {@link #location} but as a path, or {@code null} if none.
+     * Stored separately because conversion from path to URI back to path is not
+     * looseness (relative paths become absolutes).
+     */
+    private final Path path;
+
+    private boolean parsed = false;
+    private final List<Resource> components = new ArrayList<>();
+
+    CoverageJsonStore(CoverageJsonStoreProvider provider, StorageConnector connector) throws DataStoreException {
+        super(provider, connector);
+        location = connector.getStorageAs(URI.class);
+        path = connector.getStorageAs(Path.class);
+    }
+
+    @Override
+    public Optional<ParameterValueGroup> getOpenParameters() {
+        return Optional.ofNullable(URIDataStore.parameters(provider, location));
+    }
+
+    @Override
+    public Metadata getMetadata() throws DataStoreException {
+        final MetadataBuilder builder = new MetadataBuilder();
+        builder.addIdentifier(null, IOUtilities.filename(path), MetadataBuilder.Scope.ALL);
+        return builder.buildAndFreeze();
+    }
+
+    @Override
+    public synchronized Collection<? extends Resource> components() throws DataStoreException {
+        if (!parsed) {
+            parsed = true;
+            if (Files.exists(path)) {
+                try (final Jsonb b = JsonbBuilder.create();
+                     final InputStream in = new BufferedInputStream(Files.newInputStream(path))) {
+                    final CoverageJsonObject obj = b.fromJson(in, CoverageJsonObject.class);
+
+                    if (obj instanceof Coverage) {
+                        final Coverage coverage = (Coverage) obj;
+                        components.add(new CoverageResource(this, coverage));
+
+                    } else if (obj instanceof CoverageCollection) {
+                        throw new UnsupportedOperationException("Coverage collection not supported yet.");
+                    }
+
+                } catch (Exception ex) {
+                    throw new DataStoreException("Failed to parse coverage json object.", ex);
+                }
+            }
+        }
+
+        return Collections.unmodifiableList(components);
+    }
+
+
+    @Override
+    public Resource add(Resource resource) throws DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public void remove(Resource resource) throws DataStoreException {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public void close() throws DataStoreException {
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreProvider.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreProvider.java
new file mode 100644
index 0000000000..b9c48eb7f0
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreProvider.java
@@ -0,0 +1,99 @@
+/*
+ * 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.coveragejson;
+
+import java.net.URI;
+import java.util.logging.Logger;
+import org.apache.sis.internal.storage.Capability;
+import org.apache.sis.internal.storage.StoreMetadata;
+import org.apache.sis.internal.storage.URIDataStore;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreProvider;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.util.Version;
+import org.opengis.parameter.ParameterDescriptorGroup;
+
+/**
+ * The provider of {@link CoverageJsonStore} instances. Given a {@link StorageConnector} input,
+ * this class tries to instantiate a {@code CoverageJsonStore}.
+ *
+ * Draft specification : https://github.com/opengeospatial/CoverageJSON
+ *
+ * @author Johann Sorel (Geomatys)
+ *
+ * @see CoverageJsonStore
+ */
+@StoreMetadata(formatName    = CoverageJsonStoreProvider.NAME,
+               fileSuffixes  = {"covjson"},
+               capabilities  = Capability.READ,
+               resourceTypes = {Aggregate.class, GridCoverageResource.class})
+public class CoverageJsonStoreProvider extends DataStoreProvider {
+
+    public static final String NAME = "CoverageJSON";
+    /**
+     * The MIME type for Coverage-JSON files.
+     */
+    private static final String MIME_TYPE = "application/vnd.cov+json";
+
+    /**
+     * The logger used by Coverage-JSON stores.
+     *
+     * @see #getLogger()
+     */
+    private static final Logger LOGGER = Logger.getLogger("org.apache.sis.internal.coveragejson");
+
+    /**
+     * The parameter descriptor to be returned by {@link #getOpenParameters()}.
+     */
+    private static final ParameterDescriptorGroup OPEN_DESCRIPTOR = URIDataStore.Provider.descriptor(NAME);
+
+    @Override
+    public String getShortName() {
+        return NAME;
+    }
+
+    @Override
+    public ParameterDescriptorGroup getOpenParameters() {
+        return OPEN_DESCRIPTOR;
+    }
+
+    @Override
+    public ProbeResult probeContent(StorageConnector connector) throws DataStoreException {
+        final URI uri = connector.getStorageAs(URI.class);
+        if (uri != null && uri.toString().toLowerCase().endsWith(".covjson")) {
+            return new ProbeResult(true, MIME_TYPE, new Version("0.9"));
+        }
+        return ProbeResult.UNSUPPORTED_STORAGE;
+    }
+
+    @Override
+    public DataStore open(StorageConnector connector) throws DataStoreException {
+        return new CoverageJsonStore(this, connector);
+    }
+
+    /**
+     * {@return the logger used by CoverageJSON stores}.
+     */
+    @Override
+    public Logger getLogger() {
+        return LOGGER;
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java
new file mode 100644
index 0000000000..50f7e9d56c
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/CoverageResource.java
@@ -0,0 +1,457 @@
+/*
+ * 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.coveragejson;
+
+import java.awt.image.DataBuffer;
+import java.awt.image.DataBufferDouble;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
+import java.time.format.SignStyle;
+import java.time.temporal.ChronoField;
+import java.time.temporal.TemporalAccessor;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import javax.measure.Unit;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.BufferedGridCoverage;
+import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridRoundingMode;
+import org.apache.sis.internal.coveragejson.binding.Axe;
+import org.apache.sis.internal.coveragejson.binding.Axes;
+import org.apache.sis.internal.coveragejson.binding.Coverage;
+import org.apache.sis.internal.coveragejson.binding.CoverageJsonObject;
+import org.apache.sis.internal.coveragejson.binding.Domain;
+import org.apache.sis.internal.coveragejson.binding.GeographicCRS;
+import org.apache.sis.internal.coveragejson.binding.IdentifierRS;
+import org.apache.sis.internal.coveragejson.binding.NdArray;
+import org.apache.sis.internal.coveragejson.binding.Parameter;
+import org.apache.sis.internal.coveragejson.binding.ProjectedCRS;
+import org.apache.sis.internal.coveragejson.binding.ReferenceSystemConnection;
+import org.apache.sis.internal.coveragejson.binding.TemporalRS;
+import org.apache.sis.internal.coveragejson.binding.VerticalCRS;
+import org.apache.sis.measure.Units;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.matrix.MatrixSIS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.storage.AbstractGridCoverageResource;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.NoSuchDataException;
+import org.opengis.metadata.spatial.DimensionNameType;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransform1D;
+import org.opengis.util.FactoryException;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+final class CoverageResource extends AbstractGridCoverageResource {
+
+    private static final DateTimeFormatter YEAR = new DateTimeFormatterBuilder()
+                .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                .toFormatter();
+
+
+    private static final DateTimeFormatter YEAR_MONTH = new DateTimeFormatterBuilder()
+                .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                .appendLiteral('-')
+                .appendValue(ChronoField.MONTH_OF_YEAR, 1)
+                .toFormatter();
+
+    private static final DateTimeFormatter YEAR_MONTH_DAY = new DateTimeFormatterBuilder()
+                .appendValue(ChronoField.YEAR, 1, 19, SignStyle.EXCEEDS_PAD)
+                .appendLiteral('-')
+                .appendValue(ChronoField.MONTH_OF_YEAR, 1)
+                .appendLiteral('-')
+                .appendValue(ChronoField.DAY_OF_MONTH, 1)
+                .toFormatter();
+
+    private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ISO_DATE_TIME;
+
+    private final CoverageJsonStore store;
+    private final Coverage binding;
+
+    private final GridGeometry gridGeometry;
+    private final List<SampleDimension> sampleDimensions;
+    private final Map<String,double[]> datas;
+
+    public CoverageResource(CoverageJsonStore store, Coverage binding) throws DataStoreException {
+        super(null);
+        this.store = store;
+        this.binding = binding;
+
+        //rebuild grid geometry
+        try {
+            gridGeometry = jsonToGridGeometry(binding.domain);
+        } catch (FactoryException ex) {
+            throw new DataStoreException("Failed to create GridGeometry from JSON Domain", ex);
+        }
+        //rebuild sample dimensions
+        sampleDimensions = new ArrayList<>();
+        for (Entry<String,Parameter> entry : binding.parameters.any.entrySet()) {
+            final SampleDimension sd = jsonToSampleDimension(entry.getKey(), entry.getValue());
+            sampleDimensions.add(sd);
+        }
+        if (binding.parameterGroups != null) {
+            throw new UnsupportedOperationException("Parameter groups not supported yet.");
+        }
+        //read datas
+        datas = new HashMap<>();
+        for (Entry<String,NdArray> entry : binding.ranges.any.entrySet()) {
+            datas.put(entry.getKey(), jsonToDataBuffer(entry.getValue()));
+        }
+    }
+
+    @Override
+    public GridGeometry getGridGeometry() throws DataStoreException {
+        return gridGeometry;
+    }
+
+    @Override
+    public List<SampleDimension> getSampleDimensions() throws DataStoreException {
+        return Collections.unmodifiableList(sampleDimensions);
+    }
+
+    @Override
+    public GridCoverage read(GridGeometry domain, int... ranges) throws DataStoreException {
+
+        final GridGeometry intersection;
+        if (domain != null) {
+            try {
+                intersection = gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain).build();
+            } catch (DisjointExtentException ex) {
+                throw new NoSuchDataException(ex);
+            }
+        } else {
+            intersection = gridGeometry;
+        }
+
+        final double[][] rawDatas;
+        final List<SampleDimension> selected;
+        if (ranges == null || ranges.length == 0) {
+            selected = sampleDimensions;
+            rawDatas = new double[sampleDimensions.size()][0];
+            for (int i = 0; i < rawDatas.length; i++) {
+                rawDatas[i] = datas.get(sampleDimensions.get(i).getName().toString());
+            }
+
+        } else {
+            selected = new ArrayList<>();
+            rawDatas = new double[ranges.length][0];
+            for (int i = 0; i < rawDatas.length; i++) {
+                final SampleDimension sd = sampleDimensions.get(ranges[i]);
+                selected.add(sd);
+                rawDatas[i] = datas.get(sd.getName().toString());
+            }
+        }
+
+        final DataBuffer buffer = new DataBufferDouble(rawDatas, rawDatas[0].length);
+        return new BufferedGridCoverage(intersection, selected, buffer);
+    }
+
+    /**
+     * Transform JSON domain to GridGeometry.
+     */
+    private static GridGeometry jsonToGridGeometry(Domain domain) throws DataStoreException, FactoryException {
+
+        if (Domain.DOMAINTYPE_GRID.equalsIgnoreCase(domain.domainType)) {
+
+            //build coordinate system
+            final List<ReferenceSystemConnection> referencing = domain.referencing;
+            final List<String> axeNames = new ArrayList<>();
+            final List<CoordinateReferenceSystem> crss = new ArrayList<>();
+            if (referencing != null && !referencing.isEmpty()) {
+                for (ReferenceSystemConnection rsc : referencing) {
+                    axeNames.addAll(rsc.coordinates);
+                    final CoordinateReferenceSystem crs = jsonToCoordinateReferenceSystem(rsc.system);
+                    if (crs.getCoordinateSystem().getDimension() != rsc.coordinates.size()) {
+                        throw new DataStoreException("Declared CRS " + rsc.system.toString() + " do not match coordinates length");
+                    }
+                    crss.add(crs);
+                }
+            } else {
+                throw new DataStoreException("Coverage domain must be defined, Coverage as part of CoverageCollection not supported yet.");
+            }
+
+            //build extent
+            final int dimension = axeNames.size();
+            final Axes axes = domain.axes;
+            final GridGeometry[] axeGrids = new GridGeometry[dimension];
+
+            //check if axes declared on crs are ordered in the same way as the grid extent.
+            final int[] reorder = new int[dimension];
+            boolean inOrder = true;
+
+            for (int i = 0; i < dimension; i++) {
+                final String axeName = axeNames.get(i);
+                final Axe axe;
+                final int realIdx;
+                switch (axeName) {
+                    case "x" :
+                        if (axes.x == null) throw new DataStoreException("X axe is undefined");
+                        axe = axes.x;
+                        realIdx = 0;
+                        reorder[i] = realIdx;
+                        inOrder &= (i == realIdx);
+                        break;
+                    case "y" :
+                        if (axes.y == null) throw new DataStoreException("Y axe is undefined");
+                        axe = axes.y;
+                        realIdx = 1;
+                        reorder[i] = realIdx;
+                        inOrder &= (i == realIdx);
+                        break;
+                    case "z" :
+                        if (axes.z == null) throw new DataStoreException("Z axe is undefined");
+                        axe = axes.z;
+                        realIdx = 2;
+                        reorder[i] = realIdx;
+                        inOrder &= (i == realIdx);
+                        break;
+                    case "t" :
+                        if (axes.t == null) throw new DataStoreException("T axe is undefined");
+                        axe = axes.t;
+                        realIdx = reorder.length == 3 ? 2 : 3;
+                        reorder[i] = realIdx;
+                        inOrder &= (i == realIdx);
+                        break;
+                    default: throw new DataStoreException("Unexpected axe name :" + axeName);
+                }
+                axeGrids[realIdx] = jsonAxeToGridGeometry(axeName, axe);
+            }
+
+            final DimensionNameType[] dnt = new DimensionNameType[dimension];
+            final long[] lower = new long[dimension];
+            final long[] upper = new long[dimension]; //inclusive
+            MathTransform gridToCrs = null;
+            for (int i = 0; i < dimension; i++) {
+                dnt[i] = axeGrids[i].getExtent().getAxisType(0).get();
+                upper[i] = axeGrids[i].getExtent().getHigh(0);
+                if (gridToCrs == null) {
+                    gridToCrs = axeGrids[i].getGridToCRS(PixelInCell.CELL_CENTER);
+                } else {
+                    gridToCrs = MathTransforms.compound(gridToCrs, axeGrids[i].getGridToCRS(PixelInCell.CELL_CENTER));
+                }
+            }
+
+
+            if (!inOrder) {
+                final MatrixSIS m = Matrices.createZero(dimension+1, dimension+1);
+                for (int i = 0; i < dimension; i++) {
+                    m.setElement(i, reorder[i], 1.0);
+                }
+                m.setElement(dimension, dimension, 1.0);
+                final MathTransform reorderTrs = MathTransforms.linear(m);
+                gridToCrs = MathTransforms.concatenate(reorderTrs, gridToCrs);
+            }
+
+
+            final CoordinateReferenceSystem crs = CRS.compound(crss.toArray(CoordinateReferenceSystem[]::new));
+            final GridExtent extent = new GridExtent(dnt, lower, upper, true);
+            return new GridGeometry(extent, PixelInCell.CELL_CENTER, gridToCrs, crs);
+
+        } else {
+            throw new DataStoreException("Unsupported domain type " + domain.domainType);
+        }
+    }
+
+    /**
+     * Transform JSON axe to 1D GridGeometry without CRS.
+     */
+    private static GridGeometry jsonAxeToGridGeometry(String axeName, Axe axe) throws DataStoreException {
+
+        if (axe.dataType == null || Axe.DATATYPE_PRIMITIVE.equals(axe.dataType)) {
+
+        } else if (Axe.DATATYPE_TUPLE.equals(axe.dataType) ) {
+            throw new UnsupportedOperationException("Tuple axe data type not supported yet.");
+        } else if (Axe.DATATYPE_POLYGON.equals(axe.dataType) ) {
+            throw new UnsupportedOperationException("Polygon axe data type not supported yet.");
+        } else {
+            throw new DataStoreException("Unexpected axe data type :" + axe.dataType);
+        }
+
+        //rebuild axe transform
+        final MathTransform1D axeTrs;
+        final int size;
+        if (axe.values != null) {
+            final double[] values = new double[axe.values.size()];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = asDouble(axe.values.get(i));
+            }
+            size = values.length;
+            axeTrs = MathTransforms.interpolate(null, values);
+        } else if (axe.start != null) {
+            size = axe.num;
+            if (axe.num == 1) {
+                axeTrs = (MathTransform1D) MathTransforms.linear(1.0, axe.start);
+            } else {
+                final double step = (axe.stop - axe.start) / (axe.num -1);
+                axeTrs = (MathTransform1D) MathTransforms.linear(step, axe.start);
+            }
+        } else {
+            throw new DataStoreException("Axe must have values or star/stop values");
+        }
+
+        final GridExtent extent = new GridExtent(new DimensionNameType[]{DimensionNameType.valueOf(axeName)}, new long[]{0}, new long[]{size-1}, true);
+        return new GridGeometry(extent, PixelInCell.CELL_CENTER, axeTrs, null);
+    }
+
+    /**
+     * Transform JSON system object to CoordinateReferenceSystem.
+     */
+    private static CoordinateReferenceSystem jsonToCoordinateReferenceSystem(CoverageJsonObject obj) throws FactoryException {
+        if (obj instanceof GeographicCRS) {
+            final GeographicCRS jcrs = (GeographicCRS) obj;
+            if (jcrs.id != null) {
+                if (jcrs.id.equals("http://www.opengis.net/def/crs/EPSG/0/4979")) {
+                    return CommonCRS.WGS84.geographic3D();
+                }
+                return CRS.forCode(jcrs.id);
+            } else {
+                throw new UnsupportedOperationException("Geographic CRS wihout id not supported");
+            }
+        } else if (obj instanceof ProjectedCRS) {
+            final ProjectedCRS jcrs = (ProjectedCRS) obj;
+            throw new UnsupportedOperationException("ProjectedCRS not supported yet");
+
+        } else if (obj instanceof VerticalCRS) {
+            final VerticalCRS jcrs = (VerticalCRS) obj;
+            throw new UnsupportedOperationException("VerticalCRS not supported yet");
+
+        } else if (obj instanceof TemporalRS) {
+            final TemporalRS jcrs = (TemporalRS) obj;
+            if (jcrs.timeScale != null) {
+                throw new UnsupportedOperationException("TemporalRS timeScale not supported yet");
+            }
+            if ("Gregorian".equalsIgnoreCase(jcrs.calendar)) {
+                return CommonCRS.Temporal.JAVA.crs();
+            } else {
+                throw new UnsupportedOperationException(jcrs.calendar + "calendar not supported yet");
+            }
+
+        } else if (obj instanceof IdentifierRS) {
+            final IdentifierRS jcrs = (IdentifierRS) obj;
+            throw new UnsupportedOperationException("IdentifierRS not supported yet");
+
+        } else {
+            throw new UnsupportedOperationException("Unsupported system " + String.valueOf(obj));
+        }
+    }
+
+    /**
+     * Transform JSON parameter to SampleDimension.
+     */
+    private static SampleDimension jsonToSampleDimension(String name, Parameter parameter) {
+        final SampleDimension.Builder builder = new SampleDimension.Builder();
+
+        builder.setName(name);
+
+//        if (parameter.id != null) {
+//            builder.setName(parameter.id);
+//        } else if (parameter.label != null) {
+//            builder.setName(parameter.label);
+//        } else if (parameter.description != null) {
+//            builder.setName(parameter.description);
+//        }
+
+        Unit unit = jsonToUnit(parameter.unit);
+
+        //TODO categories
+        //parameter.categoryEncoding;
+        //parameter.observedProperty;
+
+        return builder.build();
+    }
+
+    /**
+     * Transform JSON unit to SIS Unit.
+     */
+    private static Unit jsonToUnit(org.apache.sis.internal.coveragejson.binding.Unit unit) {
+        if (unit == null) return Units.UNITY;
+
+        if (unit.symbol instanceof String) {
+            return Units.valueOf(unit.symbol.toString());
+        }
+        return Units.UNITY;
+    }
+
+    /**
+     * Transform JSON NdArray to number array.
+     */
+    private static double[] jsonToDataBuffer(NdArray array) throws DataStoreException {
+        //TODO more work on checking axes order
+        double[] values = new double[array.values.size()];
+        for (int i = 0; i < values.length; i++) {
+            values[i] = asDouble(array.values.get(i));
+        }
+
+        return values;
+    }
+
+    private static double asDouble(Object cdt) throws DataStoreException {
+        if (cdt == null) {
+            return Double.NaN;
+        } else if (cdt instanceof String) {
+            final Instant instant = parseDataTime(String.valueOf(cdt));
+            return instant.toEpochMilli();
+        } else if (cdt instanceof Number) {
+            return ((Number) cdt).doubleValue();
+        } else {
+            throw new DataStoreException("Unexpected value : " + cdt);
+        }
+    }
+
+    /**
+     * If the calendar is based on years, months, days, then the referenced
+     * values SHOULD use one of the following ISO8601-based lexical representations:
+     * YYYY
+     * ±XYYYY (where X stands for extra year digits)
+     * YYYY-MM
+     * YYYY-MM-DD
+     * YYYY-MM-DDTHH:MM:SS[.F]Z where Z is either “Z” or a time scale offset +|-HH:MM
+     *
+     * If calendar dates with reduced precision are used in a lexical
+     * representation (e.g. "2016"), then a client SHOULD interpret those dates
+     * in that reduced precision.
+     */
+    private static Instant parseDataTime(String str) throws DataStoreException {
+
+        for (DateTimeFormatter dtf : Arrays.asList(YEAR,YEAR_MONTH, YEAR_MONTH_DAY, DATE_TIME)) {
+            try {
+                TemporalAccessor accesser = dtf.parse(str);
+                return Instant.from(accesser);
+            } catch (DateTimeParseException ex) {
+                //do nothing
+            }
+        }
+        throw new DataStoreException("Unable to parse date : " + str);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axe.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axe.java
new file mode 100644
index 0000000000..1e8533c3b7
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axe.java
@@ -0,0 +1,136 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * An axis object MUST have either a "values" member or, as a compact notation
+ * for a regularly spaced numeric axis, have all the members "start", "stop",
+ * and "num".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"start","stop","num","dataType","coordinates","values","bounds"})
+public final class Axe extends Dictionary<Object> {
+
+    public static final String DATATYPE_PRIMITIVE = "primitive";
+    public static final String DATATYPE_TUPLE = "tuple";
+    public static final String DATATYPE_POLYGON = "polygon";
+
+    /**
+     * The values of "start" and "stop" MUST be numbers
+     */
+    public Double start;
+    /**
+     * The values of "start" and "stop" MUST be numbers
+     */
+    public Double stop;
+    /**
+     * the value of "num" an integer greater than zero.
+     *
+     * If the value of "num" is 1, then "start" and "stop" MUST have identical
+     * values. For num > 1, the array elements of "values" MAY be reconstructed
+     * with the formula start + i * step where i is the ith element and in the
+     * interval [0, num-1] and step = (stop - start) / (num - 1).
+     *
+     * If num = 1 then "values" is [start]. Note that "start" can be greater
+     * than "stop" in which case the axis values are descending.
+     */
+    public Integer num;
+    /**
+     * The value of "dataType" determines the structure of an axis value and its
+     * coordinates that are made available for referencing. The values of
+     * "dataType" defined in this Community Standard are "primitive", "tuple",
+     * and "polygon". Custom values MAY be used as detailed in the Extensions
+     * section. For "primitive", there is a single coordinate identifier and
+     * each axis value MUST be a number or string. For "tuple", each axis value
+     * MUST be an array of fixed size of primitive values in a defined order,
+     * where the tuple size corresponds to the number of coordinate identifiers.
+     * For "polygon", each axis value MUST be a GeoJSON Polygon coordinate
+     * array, where the order of coordinates is given by the "coordinates"
+     * array.
+     *
+     * If missing, the member "dataType" defaults to "primitive" and MUST not be
+     * included for that default case.
+     *
+     * If "dataType" is "primitive" and the associated reference system (see
+     * 6.1.2) defines a natural ordering of values then the array values in
+     * "values", if existing, MUST be ordered monotonically, that is, increasing
+     * or decreasing.
+     */
+    public String dataType;
+    /**
+     * The value of "coordinates" is a non-empty array of coordinate identifiers
+     * corresponding to the order of the coordinates defined by "dataType".
+     *
+     * If missing, the member "coordinates" defaults to a one-element array of
+     * the axis identifier and MUST NOT be included for that default case.
+     *
+     * A coordinate identifier SHALL NOT be defined more than once in all axis
+     * objects of a domain object.
+     */
+    public List<String> coordinates;
+    /**
+     * The value of "values" is a non-empty array of axis values.
+     */
+    public List<Object> values;
+    /**
+     * An axis object MAY have axis value bounds defined in the member "bounds"
+     * where the value is an array of values of length len*2 with len being the
+     * length of the "values" array. For each axis value at array index i in the
+     * "values" array, a lower and upper bounding value at positions 2*i and
+     * 2*i+1, respectively, are given in the bounds array.
+     *
+     * If a domain axis object has no "bounds" member then a bounds array MAY be
+     * derived automatically.
+     */
+    public List<Object> bounds;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Axe)) return false;
+
+        final Axe cdt = ((Axe) other);
+        return super.equals(other)
+            && Objects.equals(start, cdt.start)
+            && Objects.equals(stop, cdt.stop)
+            && Objects.equals(num, cdt.num)
+            && Objects.equals(dataType, cdt.dataType)
+            && Objects.equals(coordinates, cdt.coordinates)
+            && Objects.equals(values, cdt.values)
+            && Objects.equals(bounds, cdt.bounds);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                start,
+                stop,
+                num,
+                dataType,
+                coordinates,
+                values,
+                bounds);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axes.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axes.java
new file mode 100644
index 0000000000..1f34d2c25c
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Axes.java
@@ -0,0 +1,58 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * The "axes" member MUST NOT be empty.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"x","y","z","t"})
+public final class Axes extends Dictionary<Object> {
+
+    public Axe x;
+    public Axe y;
+    public Axe z;
+    public Axe t;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Axes)) return false;
+
+        final Axes cdt = ((Axes) other);
+        return super.equals(other)
+            && Objects.equals(x, cdt.x)
+            && Objects.equals(y, cdt.y)
+            && Objects.equals(y, cdt.y)
+            && Objects.equals(t, cdt.t);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                x,
+                y,
+                z,
+                t);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Category.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Category.java
new file mode 100644
index 0000000000..cd2e7d14b3
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Category.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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * A category object MUST an "id" and a "label" member, and MAY have a
+ * "description" member.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"id","label","description"})
+public final class Category extends Dictionary<Object> {
+
+    /**
+     * The value of "id" MUST be a string and SHOULD be a common identifier.
+     */
+    public String id;
+    /**
+     * The value of "label" MUST be an i18n object of the name of the
+     * category and SHOULD be short.
+     */
+    public I18N label;
+    /**
+     * If given, the value of "description" MUST be an i18n object with a
+     * textual description of the* category.
+     */
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Category)) return false;
+
+        final Category cdt = ((Category) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CategoryEncoding.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CategoryEncoding.java
new file mode 100644
index 0000000000..94cf4a3cc0
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CategoryEncoding.java
@@ -0,0 +1,31 @@
+/*
+ * 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.coveragejson.binding;
+
+
+/**
+ * CategoryEncoding is an object where each key is equal to an "id" value of
+ * the "categories" array within the "observedProperty" member of the
+ * parameter object. There MUST be no duplicate keys. The value is either
+ * an integer or an array of integers where each integer MUST be unique
+ * within the object.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class CategoryEncoding extends Dictionary<String> {
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Coverage.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Coverage.java
new file mode 100644
index 0000000000..db43a0cc02
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Coverage.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.internal.coveragejson.binding;
+
+import jakarta.json.bind.JsonbException;
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.stream.JsonParser;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A CoverageJSON object with the type "Coverage" is a coverage object.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","domain","parameters","parameterGroups","ranges"})
+public final class Coverage extends CoverageJsonObject {
+
+    /**
+     * If a coverage has a commonly used identifier, that identifier SHOULD be
+     * included as a member of the coverage object with the name "id".
+     */
+    public String id;
+    /**
+     * A coverage object MUST have a member with the name "domain" where the
+     * value is either a domain object or a URL.
+     *
+     * If the value of "domain" is a URL and the referenced domain has a
+     * "domainType" member, then the coverage object SHOULD have the member
+     * "domainType" where the value MUST equal that of the referenced domain.
+     *
+     * If the coverage object is part of a coverage collection which has a
+     * "domainType" member then that member SHOULD be omitted in the coverage
+     * object.
+     */
+    //@JsonbTypeDeserializer(Coverage.DomainDeserializer.class)
+    //TODO should be a Domain or an URL, DomainDeserializer not working as expected
+    public Domain domain;
+    /**
+     * A coverage object MAY have a member with the name "parameters" where the
+     * value is an object where each member has as name a short identifier and
+     * as value a parameter object. The identifier corresponds to the commonly
+     * known concept of “variable name” and is merely used in clients for
+     * conveniently accessing the corresponding range object.
+     *
+     * A coverage object MUST have a "parameters" member if the coverage object
+     * is not part of a coverage collection or if the coverage collection does
+     * not have a "parameters" member.
+     */
+    public Parameters parameters;
+    /**
+     * A coverage object MAY have a member with the name "parameterGroups" where
+     * the value is an array of ParameterGroup objects.
+     */
+    public List<ParameterGroup> parameterGroups;
+    /**
+     * A coverage object MUST have a member with the name "ranges" where the
+     * value is a range set object. Any member of a range set object has as
+     * name any of the names in a "parameters" object in scope and as value
+     * either an NdArray or TiledNdArray object or a URL resolving to a
+     * CoverageJSON document of such object. A "parameters" member in scope is
+     * either within the enclosing coverage object or, if part of a coverage
+     * collection, in the parent coverage collection object. The shape and axis
+     * names of each NdArray or TiledNdArray object MUST correspond to the
+     * domain axes defined by "domain", while single-valued axes MAY be omitted.
+     * If the referenced parameter object has a "categoryEncoding" member, then
+     * each non-null array element of the "values" member of the NdArray object,
+     * or the linked NdArray objects within a TiledNdArray object, MUST be equal
+     * to one of the values defined in the "categoryEncoding" object and be
+     * interpreted as the matching category.
+     */
+    public Ranges ranges;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Coverage)) return false;
+
+        final Coverage cdt = ((Coverage) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(domain, cdt.domain)
+            && Objects.equals(parameters, cdt.parameters)
+            && Objects.equals(parameterGroups, cdt.parameterGroups)
+            && Objects.equals(ranges, cdt.ranges);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                domain,
+                parameters,
+                parameterGroups,
+                ranges);
+    }
+
+    public static class DomainDeserializer implements JsonbDeserializer<Object> {
+        @Override
+        public Object deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            final JsonParser.Event event = parser.next();
+            if (event == JsonParser.Event.START_OBJECT) {
+                // Deserialize inner object
+                return ctx.deserialize(Domain.class, parser);
+            } else if (event == JsonParser.Event.VALUE_STRING) {
+                return parser.getString();
+            } else {
+                throw new JsonbException("Unexpected json element");
+            }
+        }
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageCollection.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageCollection.java
new file mode 100644
index 0000000000..6e80503edd
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageCollection.java
@@ -0,0 +1,90 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A CoverageJSON object with the type "CoverageCollection" is a coverage collection object.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","domainType","parameters","parameterGroups","referencing","coverages"})
+public final class CoverageCollection extends CoverageJsonObject {
+
+    /**
+     * A coverage collection object MAY have the member "domainType" with a
+     * string value to indicate that the coverage collection only contains
+     * coverages of the given domain type. See the section Common Domain Types
+     * for details. Custom domain types may be used as recommended in the
+     * section Extensions.
+     *
+     * If a coverage collection object has the member "domainType", then this
+     * member is inherited to all included coverages.
+     */
+    public String domainType;
+    /**
+     * A coverage collection object MAY have a member with the name "parameters"
+     * where the value is an object where each member has as name a short
+     * identifier and as value a parameter object.
+     */
+    public Parameter parameters;
+    /**
+     * A coverage collection object MAY have a member with the name
+     * "parameterGroups" where the value is an array of ParameterGroup objects.
+     */
+    public List<ParameterGroup> parameterGroups;
+    /**
+     * A coverage collection object MAY have a member with the name "referencing"
+     * where the value is an array of reference system connection objects.
+     */
+    public List<ReferenceSystemConnection> referencing;
+    /**
+     * A coverage collection object MUST have a member with the name "coverages".
+     * The value corresponding to "coverages" is an array. Each element in the
+     * array is a coverage object as defined above.
+     */
+    public List<Coverage> coverages;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof CoverageCollection)) return false;
+
+        final CoverageCollection cdt = ((CoverageCollection) other);
+        return super.equals(other)
+            && Objects.equals(domainType, cdt.domainType)
+            && Objects.equals(parameters, cdt.parameters)
+            && Objects.equals(coverages, cdt.coverages)
+            && Objects.equals(parameterGroups, cdt.parameterGroups)
+            && Objects.equals(referencing, cdt.referencing);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                domainType,
+                coverages,
+                parameters,
+                parameterGroups,
+                referencing);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageJsonObject.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageJsonObject.java
new file mode 100644
index 0000000000..1b80490a85
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/CoverageJsonObject.java
@@ -0,0 +1,55 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbSubtype;
+import jakarta.json.bind.annotation.JsonbTypeInfo;
+
+/**
+ * A CoverageJSON document can be extended with custom members and types in a
+ * robust and interoperable way. For that, it makes use of absolute URIs and
+ * compact URIs (prefix:suffix) in order to avoid conflicts with other extensions
+ * and future versions of the format. A central registry of compact URI prefixes
+ * is provided which anyone can extend and which is a simple mapping from compact
+ * URI prefix to namespace URI in order to avoid collisions with other extensions
+ * that are based on compact URIs as well. Extensions that do not follow this
+ * approach MAY use simple names instead of absolute or compact URIs but have to
+ * accept the consequence of the document being less interoperable and future-proof.
+ * In certain use cases this is not an issue and may be a preferred solution for
+ * simplicity reasons, for example, if such CoverageJSON documents are only used
+ * internally and are not meant to be shared to a wider audience.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbTypeInfo ( key = "type",value = {
+    @JsonbSubtype(alias = "Coverage", type = Coverage.class),
+    @JsonbSubtype(alias = "CoverageCollection", type = CoverageCollection.class),
+    @JsonbSubtype(alias = "Domain", type = Domain.class),
+    @JsonbSubtype(alias = "NdArray", type = NdArray.class),
+    @JsonbSubtype(alias = "Parameter", type = Parameter.class),
+    @JsonbSubtype(alias = "ParameterGroup", type = ParameterGroup.class),
+
+    //system subtypes
+    @JsonbSubtype(alias = "GeographicCRS", type = GeographicCRS.class),
+    @JsonbSubtype(alias = "ProjectedCRS", type = ProjectedCRS.class),
+    @JsonbSubtype(alias = "IdentifierRS", type = IdentifierRS.class),
+    @JsonbSubtype(alias = "VerticalCRS", type = VerticalCRS.class),
+    @JsonbSubtype(alias = "TemporalRS", type = TemporalRS.class)
+})
+public class CoverageJsonObject extends Dictionary<Object>{
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Dictionary.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Dictionary.java
new file mode 100644
index 0000000000..b46add8557
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Dictionary.java
@@ -0,0 +1,69 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.annotation.JsonbTransient;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @author Johann Sorel (Geomatys)
+ */
+public class Dictionary<T> {
+
+    /**
+     * could be anything.
+     * TODO find how to cath any other property as in Jackson @JsonAnyGetter and Johnzon @JohnzonAny
+     */
+    @JsonbTransient
+    public final LinkedHashMap<String, T> any = new LinkedHashMap<>();
+
+    public final Map<String, T> getAny() {
+        return this.any;
+    }
+
+    public final void setAnyProperty(String name, T value) {
+        this.any.put(name, value);
+    }
+
+    @Override
+    public String toString() {
+        try (Jsonb jsonb = JsonbBuilder.create()) {
+            return jsonb.toJson(this);
+        } catch (Exception ex) {
+            ex.printStackTrace();
+            return getClass().getName() + " : to_string_exception" + ex.getMessage();
+        }
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Dictionary)) return false;
+
+        final Dictionary cdt = ((Dictionary) other);
+        return Objects.equals(any, cdt.any);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(any);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Domain.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Domain.java
new file mode 100644
index 0000000000..5291a1b09f
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Domain.java
@@ -0,0 +1,90 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A domain object is a CoverageJSON object which defines a set of positions and
+ * their extent in one or more referencing systems.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","domainType","axes","referencing"})
+public final class Domain extends CoverageJsonObject {
+
+    public static final String DOMAINTYPE_GRID = "Grid";
+    public static final String DOMAINTYPE_VERTICALPROFILE = "VerticalProfile";
+    public static final String DOMAINTYPE_POINTSERIES = "PointSeries";
+    public static final String DOMAINTYPE_POINT = "Point";
+    public static final String DOMAINTYPE_MULTIPOINTSERIES = "MultiPointSeries";
+    public static final String DOMAINTYPE_MULTIPOINT = "MultiPoint";
+    public static final String DOMAINTYPE_POLYGONSERIES = "PolygonSeries";
+    public static final String DOMAINTYPE_POLYGON = "Polygon";
+    public static final String DOMAINTYPE_MULTIPOLYGONSERIES = "MultiPolygonSeries";
+    public static final String DOMAINTYPE_MULTIPOLYGON = "MultiPolygon";
+    public static final String DOMAINTYPE_TRAJECTORY = "Trajectory";
+    public static final String DOMAINTYPE_SECTION = "Section";
+
+    /**
+     * For interoperability reasons it is RECOMMENDED that a domain object has
+     * the member "domainType" with a string value to indicate that the domain
+     * follows a certain structure (e.g. a time series, a vertical profile, a
+     * spatio-temporal 4D grid). See the section Common Domain Types for details.
+     * Custom domain types may be used as recommended in the section Extensions.
+     */
+    public String domainType;
+    /**
+     * A domain object MUST have the member "axes" which has as value an object
+     * where each key is an axis identifier and each value an axis object as defined below.
+     */
+    public Axes axes;
+    /**
+     * A domain object MAY have the member "referencing" where the value is an
+     * array of reference system connection objects as defined below.
+     *
+     * A domain object MUST have a "referencing" member if the domain object is
+     * not part of a coverage collection or if the coverage collection does not
+     * have a "referencing" member.
+     */
+    public List<ReferenceSystemConnection> referencing;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Domain)) return false;
+
+        final Domain cdt = ((Domain) other);
+        return super.equals(other)
+            && Objects.equals(domainType, cdt.domainType)
+            && Objects.equals(axes, cdt.axes)
+            && Objects.equals(referencing, cdt.referencing);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                domainType,
+                axes,
+                referencing);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/GeographicCRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/GeographicCRS.java
new file mode 100644
index 0000000000..0edc1a4420
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/GeographicCRS.java
@@ -0,0 +1,72 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Geographic CRSs anchor coordinate values to an ellipsoidal approximation of
+ * the Earth. They have coordinate axes of geodetic longitude and geodetic
+ * latitude, and perhaps height above the ellipsoid (i.e. they can be two- or three-dimensional).
+ * The origin of the CRS is on the surface of the ellipsoid.
+ *
+ * Note that sometimes (e.g. for numerical model data) the exact CRS may not be
+ * known or may be undefined. In this case the "id" may be omitted, but the "type"
+ * still indicates that this is a geographic CRS. Therefore clients can still use
+ * geodetic longitude, geodetic latitude (and maybe height) axes, even if they
+ * cannot accurately georeference the information.
+ * If a Coverage conforms to one of the defined domain types then the coordinate
+ * identifier "x" is used to denote geodetic longitude, "y" is used for geodetic
+ * latitude and "z" for ellipsoidal height.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","description"})
+public final class GeographicCRS extends CoverageJsonObject {
+
+    /**
+     * The object MAY have an "id" member, whose value MUST be a string and
+     * SHOULD be a common identifier for the reference system.
+     */
+    public String id;
+    /**
+     * The object MAY have a "description" member, where the value MUST be an
+     * i18n object, but no standardized content is interpreted from this description.
+     */
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof GeographicCRS)) return false;
+
+        final GeographicCRS cdt = ((GeographicCRS) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                description);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/I18N.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/I18N.java
new file mode 100644
index 0000000000..7ec02f650f
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/I18N.java
@@ -0,0 +1,119 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import java.lang.reflect.Type;
+import java.util.Locale;
+import java.util.Map;
+import org.apache.sis.internal.coveragejson.binding.I18N.Serializer;
+import org.opengis.util.InternationalString;
+
+/**
+ * The special language tag "und" can be used to identify a value whose language
+ * is unknown or undetermined.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbTypeDeserializer(I18N.Deserializer.class)
+@JsonbTypeSerializer(Serializer.class)
+public final class I18N extends Dictionary<String> implements InternationalString {
+
+    public static final String UNDETERMINED = "und";
+
+    public I18N() {
+    }
+
+    public I18N(String lang, String text) {
+        setAnyProperty(lang, text);
+    }
+
+    private String getDefault() {
+        String str = any.get(UNDETERMINED);
+        if (str == null && !any.isEmpty()) str = any.get(any.keySet().iterator().next());
+        if (str == null) str = "";
+        return str;
+    }
+
+    public String toString() {
+        return getDefault();
+    }
+
+    @Override
+    public String toString(Locale locale) {
+        String str = any.get(locale.getLanguage());
+        if (str == null) str = any.get(locale.getISO3Language());
+        return getDefault();
+    }
+
+    @Override
+    public int length() {
+        return getDefault().length();
+    }
+
+    @Override
+    public char charAt(int index) {
+        return getDefault().charAt(index);
+    }
+
+    @Override
+    public CharSequence subSequence(int start, int end) {
+        return getDefault().subSequence(start, end);
+    }
+
+    @Override
+    public int compareTo(InternationalString o) {
+        return getDefault().compareTo(o.toString());
+    }
+
+    public static class Deserializer implements JsonbDeserializer<I18N> {
+        @Override
+        public I18N deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            final I18N candidate = new I18N();
+            while (parser.hasNext()) {
+                final JsonParser.Event event = parser.next();
+                if (event == JsonParser.Event.KEY_NAME) {
+                    // Deserialize inner object
+                    final String name = parser.getString();
+                    String value = ctx.deserialize(String.class, parser);
+                    candidate.setAnyProperty(name, value);
+                }
+            }
+            return candidate;
+        }
+    }
+
+    public static class Serializer implements JsonbSerializer<I18N> {
+
+        @Override
+        public void serialize(I18N ranges, JsonGenerator jg, SerializationContext sc) {
+            jg.writeStartObject();
+            for (Map.Entry<String,String> entry : ranges.any.entrySet()) {
+                jg.write(entry.getKey(), entry.getValue());
+            }
+            jg.writeEnd();
+        }
+
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/IdentifierRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/IdentifierRS.java
new file mode 100644
index 0000000000..8ff531369e
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/IdentifierRS.java
@@ -0,0 +1,84 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Identifier-based reference systems (identifier RS) .
+ *
+ * Coordinate values associated with an identifier RS MUST be strings.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","label","description","targetConcept","identifiers"})
+public final class IdentifierRS extends CoverageJsonObject {
+
+    /**
+     * An identifier RS object MAY have a member "id" where the value MUST be a
+     * string and SHOULD be a common identifier for the reference system.
+     */
+    public String id;
+    /**
+     * An identifier RS object MAY have a member "label" where the value MUST be
+     * an i18n object that is the name of the reference system.
+     */
+    public I18N label;
+    /**
+     * An identifier RS object MAY have a member "description" where the value
+     * MUST be an i18n object that is the (perhaps lengthy) description of the
+     * reference system.
+     */
+    public I18N description;
+    /**
+     * An identifier RS object MUST have a member "targetConcept"
+     */
+    public TargetConcept targetConcept;
+    /**
+     * An identifier RS object MAY have a member "identifiers".
+     */
+    public Identifiers identifiers;
+
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof IdentifierRS)) return false;
+
+        final IdentifierRS cdt = ((IdentifierRS) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description)
+            && Objects.equals(targetConcept, cdt.targetConcept)
+            && Objects.equals(identifiers, cdt.identifiers);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description,
+                targetConcept,
+                identifiers);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Identifiers.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Identifiers.java
new file mode 100644
index 0000000000..ac4f495183
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Identifiers.java
@@ -0,0 +1,29 @@
+/*
+ * 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.coveragejson.binding;
+
+
+/**
+ * An identifier RS object MAY have a member "identifiers" where the value is
+ * an object where each key is an identifier referenced by the identifier RS
+ * and each value an object describing the referenced concept, equal to "targetConcept".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public final class Identifiers extends Dictionary<TargetConcept> {
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/NdArray.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/NdArray.java
new file mode 100644
index 0000000000..dc1a6a2c1e
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/NdArray.java
@@ -0,0 +1,104 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A CoverageJSON object with the type "NdArray" is an NdArray object.
+ * It represents a multidimensional (>= 0D) array with named axes, encoded as a
+ * flat, one-dimensional JSON array in row-major order.
+ *
+ * Note that common JSON implementations use IEEE 754-2008 64-bit
+ * (double precision) floating point numbers as the data type for "values". Users
+ * SHOULD be aware of the limitations in precision when encoding numbers in this way.
+ * For example, when encoding integers, users SHOULD be aware that only values
+ * within the range [-253+1, 253-1] can be represented in a way that will ensure
+ * exact interoperability among such implementations [IETF RFC 7159].
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","dataType","axisNames","shape","values"})
+public final class NdArray extends CoverageJsonObject {
+
+    public static final String DATATYPE_FLOAT = "float";
+    public static final String DATATYPE_INTEGER = "integer";
+    public static final String DATATYPE_STRING = "string";
+
+    /**
+     * An NdArray object MUST have a member with the name "dataType" where the
+     * value is either "float", "integer", or "string" and MUST correspond to
+     * the data type of the non-null values in the "values" array.
+     */
+    public String dataType;
+    /**
+     * An NdArray object MAY have a member with the name "axisNames" where the
+     * value is an array of strings of the same length as "shape", such that
+     * each string assigns a name to the corresponding dimension. For 0D arrays,
+     * "axisNames" MAY be omitted (defaulting to []). For >= 1D arrays it MUST
+     * be included.
+     */
+    public String[] axisNames;
+    /**
+     * An NdArray object MAY have a member with the name "shape" where the value
+     * is an array of integers. For 0D arrays, "shape" MAY be omitted
+     * (defaulting to []). For >= 1D arrays it MUST be included.
+     *
+     * Where "shape" is present and non-empty, the product of its values MUST
+     * equal the number of elements in the "values" array.
+     */
+    public int[] shape;
+    /**
+     * An NdArray object MUST have a member with the name "values" where the
+     * value is a non-empty array of numbers and nulls, or strings and nulls,
+     * where nulls represent missing data.
+     *
+     * Zero-dimensional NdArrays MUST have exactly one item in the "values" array.
+     *
+     * Within the "values" array, the elements MUST be ordered such that the
+     * last dimension in "axisNames" varies fastest, i.e. row-major order.
+     * (This mimics the approach taken in NetCDF; see the example below.)
+     */
+    public List<Object> values; //because of null and string values
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof NdArray)) return false;
+
+        final NdArray cdt = ((NdArray) other);
+        return super.equals(other)
+            && Objects.equals(dataType, cdt.dataType)
+            && Arrays.equals(axisNames, cdt.axisNames)
+            && Arrays.equals(shape, cdt.shape)
+            && Objects.equals(values, cdt.values);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                dataType,
+                Arrays.hashCode(axisNames),
+                Arrays.hashCode(shape),
+                values);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ObservedProperty.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ObservedProperty.java
new file mode 100644
index 0000000000..62f49f163e
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ObservedProperty.java
@@ -0,0 +1,88 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Observed property is an object which MUST have the member "label" and which
+ * MAY have the members "id", "description", and "categories".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"id","label","description","categories"})
+public final class ObservedProperty extends Dictionary<Object> {
+
+    /**
+     * If given, the value of "id" MUST be a string and SHOULD be a common
+     * identifier.
+     */
+    public String id;
+    /**
+     * The value of "label" MUST be an i18n object that is the name of the
+     * observed property and which SHOULD be short.
+     */
+    public I18N label;
+    /**
+     * If given, the value of "description" MUST be an i18n object with a
+     * textual description of the observed property.
+     */
+    public I18N description;
+    /**
+     *
+     * If given, the value of "categories" MUST be a non-empty array of category
+     * objects.
+     */
+    public List<Category> categories;
+
+    public ObservedProperty() {
+    }
+
+    public ObservedProperty(String id, I18N label, I18N description, List<Category> categories) {
+        this.id = id;
+        this.label = label;
+        this.description = description;
+        this.categories = categories;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof ObservedProperty)) return false;
+
+        final ObservedProperty cdt = ((ObservedProperty) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description)
+            && Objects.equals(categories, cdt.categories);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description,
+                categories);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameter.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameter.java
new file mode 100644
index 0000000000..dc5c6645e4
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameter.java
@@ -0,0 +1,91 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * A parameter object MAY have any number of members (name/value pairs).
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","label","description","unit","observedProperty","categoryEncoding"})
+public final class Parameter extends CoverageJsonObject {
+
+    /**
+     * A parameter object MAY have a member with the name "id" where the value
+     * MUST be a string and SHOULD be a common identifier.
+     */
+    public String id;
+    /**
+     * A parameter object MAY have a member with the name "label" where the value
+     * MUST be an i18n object that is the name of the parameter and which SHOULD be short.
+     * Note that this SHOULD be left out if it would be identical to the "label"
+     * of the "observedProperty" member.
+     */
+    public I18N label;
+    /**
+     * A parameter object MAY have a member with the name "description" where
+     * the value MUST be an i18n object which is a, perhaps lengthy, textual
+     * description of the parameter.
+     */
+    public I18N description;
+    /**
+     * A parameter object MAY have a member with the name "unit".
+     * A parameter object MUST NOT have a "unit" member if the "observedProperty"
+     * member has a "categories" member.
+     */
+    public Unit unit;
+    /**
+     * A parameter object MUST have a member with the name "observedProperty".
+     */
+    public ObservedProperty observedProperty;
+    /**
+     * A parameter object MAY have a member with the name "categoryEncoding".
+     */
+    public CategoryEncoding categoryEncoding;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Parameter)) return false;
+
+        final Parameter cdt = ((Parameter) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description)
+            && Objects.equals(unit, cdt.unit)
+            && Objects.equals(observedProperty, cdt.observedProperty)
+            && Objects.equals(categoryEncoding, cdt.categoryEncoding);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description,
+                unit,
+                observedProperty,
+                categoryEncoding);
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ParameterGroup.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ParameterGroup.java
new file mode 100644
index 0000000000..d18ddfd3ed
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ParameterGroup.java
@@ -0,0 +1,86 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A parameter group object MUST have either or both the members "label" or/and "observedProperty".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","label","description","observedProperty","members"})
+public final class ParameterGroup extends CoverageJsonObject {
+
+    /**
+     * A parameter group object MAY have a member with the name "id" where the
+     * value MUST be a string and SHOULD be a common identifier.
+     */
+    public String id;
+    /**
+     * A parameter group object MAY have a member with the name "label" where
+     * the value MUST be an i18n object that is the name of the parameter group
+     * and which SHOULD be short. Note that this SHOULD be left out if it would
+     * be identical to the "label" of the "observedProperty" member.
+     */
+    public I18N label;
+    /**
+     * A parameter group object MAY have a member with the name "description"
+     * where the value MUST be an i18n object which is a, perhaps lengthy,
+     * textual description of the parameter group.
+     */
+    public String description;
+    /**
+     * A parameter group object MAY have a member with the name "observedProperty"
+     * where the value is an object as specified for parameter objects.
+     */
+    public ObservedProperty observedProperty;
+    /**
+     * A parameter group object MUST have a member with the name "members"
+     * where the value is a non-empty array of parameter identifiers
+     * (see 6.3 Coverage objects).
+     */
+    public String[] members;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof ParameterGroup)) return false;
+
+        final ParameterGroup cdt = ((ParameterGroup) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description)
+            && Objects.equals(observedProperty, cdt.observedProperty)
+            && Arrays.equals(members, cdt.members);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                description,
+                observedProperty,
+                Arrays.hashCode(members));
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameters.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameters.java
new file mode 100644
index 0000000000..7c29c5fb26
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Parameters.java
@@ -0,0 +1,71 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import jakarta.json.stream.JsonParser.Event;
+import java.lang.reflect.Type;
+import java.util.Map;
+import org.apache.sis.internal.coveragejson.binding.Parameters.Deserializer;
+import org.apache.sis.internal.coveragejson.binding.Parameters.Serializer;
+
+/**
+ * Constains a map of parameter objects.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbTypeDeserializer(Deserializer.class)
+@JsonbTypeSerializer(Serializer.class)
+public final class Parameters extends Dictionary<Parameter> {
+
+    public static class Deserializer implements JsonbDeserializer<Parameters> {
+        @Override
+        public Parameters deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            final Parameters parameters = new Parameters();
+            while (parser.hasNext()) {
+                final Event event = parser.next();
+                if (event == JsonParser.Event.KEY_NAME) {
+                    // Deserialize inner object
+                    final String name = parser.getString();
+                    final Parameter value = ctx.deserialize(Parameter.class, parser);
+                    parameters.setAnyProperty(name, value);
+                }
+            }
+            return parameters;
+        }
+    }
+
+    public static class Serializer implements JsonbSerializer<Parameters> {
+
+        @Override
+        public void serialize(Parameters parameters, JsonGenerator jg, SerializationContext sc) {
+            jg.writeStartObject();
+            for (Map.Entry<String,Parameter> entry : parameters.any.entrySet()) {
+                sc.serialize(entry.getKey(), entry.getValue(), jg);
+            }
+            jg.writeEnd();
+        }
+
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ProjectedCRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ProjectedCRS.java
new file mode 100644
index 0000000000..ebff794f48
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ProjectedCRS.java
@@ -0,0 +1,64 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Projected CRSs use two coordinates to denote positions on a Cartesian plane,
+ * which is derived from projecting the ellipsoid according to some defined transformation.
+ *
+ * If a Coverage conforms to one of the defined domain types then the coordinate
+ * identifier "x" is used to denote easting and "y" is used for northing.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","description"})
+public final class ProjectedCRS extends CoverageJsonObject {
+
+    /**
+     * The object MAY have an "id" member, whose value MUST be a string and
+     * SHOULD be a common identifier for the reference system.
+     */
+    public String id;
+    /**
+     * The object MAY have a "description" member, where the value MUST be an
+     * i18n object, but no standardized content is interpreted from this description.
+     */
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof ProjectedCRS)) return false;
+
+        final ProjectedCRS cdt = ((ProjectedCRS) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                description);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Ranges.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Ranges.java
new file mode 100644
index 0000000000..3a322ad65a
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Ranges.java
@@ -0,0 +1,67 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbTypeDeserializer;
+import jakarta.json.bind.annotation.JsonbTypeSerializer;
+import jakarta.json.bind.serializer.DeserializationContext;
+import jakarta.json.bind.serializer.JsonbDeserializer;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import jakarta.json.stream.JsonParser;
+import java.lang.reflect.Type;
+import java.util.Map.Entry;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbTypeDeserializer(Ranges.Deserializer.class)
+@JsonbTypeSerializer(Ranges.Serializer.class)
+public final class Ranges extends Dictionary<NdArray> {
+
+    public static class Deserializer implements JsonbDeserializer<Ranges> {
+        @Override
+        public Ranges deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) {
+            final Ranges candidate = new Ranges();
+            while (parser.hasNext()) {
+                final JsonParser.Event event = parser.next();
+                if (event == JsonParser.Event.KEY_NAME) {
+                    // Deserialize inner object
+                    final String name = parser.getString();
+                    final NdArray value = ctx.deserialize(NdArray.class, parser);
+                    candidate.setAnyProperty(name, value);
+                }
+            }
+            return candidate;
+        }
+    }
+
+    public static class Serializer implements JsonbSerializer<Ranges> {
+
+        @Override
+        public void serialize(Ranges ranges, JsonGenerator jg, SerializationContext sc) {
+            jg.writeStartObject();
+            for (Entry<String,NdArray> entry : ranges.any.entrySet()) {
+                sc.serialize(entry.getKey(), entry.getValue(), jg);
+            }
+            jg.writeEnd();
+        }
+
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ReferenceSystemConnection.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ReferenceSystemConnection.java
new file mode 100644
index 0000000000..c072bf206d
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/ReferenceSystemConnection.java
@@ -0,0 +1,67 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A reference system connection object creates a link between values within
+ * domain axes and a reference system to be able to interpret those values, e.g.
+ * as coordinates in a certain coordinate reference system.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"coordinates","system"})
+public final class ReferenceSystemConnection extends Dictionary<Object> {
+
+    /**
+     * A reference system connection object MUST have a member "coordinates"
+     * which has as value an array of coordinate identifiers that are referenced
+     * in this object. Depending on the type of referencing, the ordering of the
+     * identifiers MAY be relevant, e.g. for 2D/3D coordinate reference systems.
+     * In this case, the order of the identifiers MUST match the order of axes
+     * in the coordinate reference system.
+     */
+    public List<String> coordinates;
+    /**
+     * A reference system connection object MUST have a member "system" whose
+     * value MUST be a Reference System Object.
+     */
+    public CoverageJsonObject system;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof ReferenceSystemConnection)) return false;
+
+        final ReferenceSystemConnection cdt = ((ReferenceSystemConnection) other);
+        return super.equals(other)
+            && Objects.equals(system, cdt.system)
+            && Objects.equals(coordinates, cdt.coordinates);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                system,
+                coordinates);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Symbol.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Symbol.java
new file mode 100644
index 0000000000..5621f9be4d
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Symbol.java
@@ -0,0 +1,60 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"value","type"})
+public final class Symbol extends Dictionary<Object> {
+
+    /**
+     * "value" is the symbolic unit notation
+     */
+    public String value;
+    /**
+     * "type" references the unit serialization scheme that is used. "type" MUST
+     * HAVE the value "http://www.opengis.net/def/uom/UCUM/" if UCUM is used, or
+     * a custom value as recommended in section Extensions.
+     */
+    public String type;
+
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Symbol)) return false;
+
+        final Symbol cdt = ((Symbol) other);
+        return super.equals(other)
+            && Objects.equals(value, cdt.value)
+            && Objects.equals(type, cdt.type);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                value,
+                type);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TargetConcept.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TargetConcept.java
new file mode 100644
index 0000000000..e56b177317
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TargetConcept.java
@@ -0,0 +1,54 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * TargetConcept is an object that MUST have a member "label" and MAY have a member
+ * "description" where the value of each MUST be an i18n object that is the
+ * name or description, respectively, of the concept which is referenced in the system.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"label","description"})
+public final class TargetConcept extends Dictionary<Object> {
+
+    public I18N label;
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof TargetConcept)) return false;
+
+        final TargetConcept cdt = ((TargetConcept) other);
+        return super.equals(other)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                label,
+                description);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TemporalRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TemporalRS.java
new file mode 100644
index 0000000000..d3eacc3159
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TemporalRS.java
@@ -0,0 +1,79 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Time is referenced by a temporal reference system (temporal RS). In the current
+ * version of this Community Standard, only a string-based notation for time
+ * values is defined. Future versions of this Community Standard may allow for
+ * alternative notations, such as recording time values as numeric offsets from
+ * a given temporal datum (e.g. “days since 1970-01-01”).
+ *
+ * If the calendar is based on years, months, days, then the referenced values
+ * SHOULD use one of the following ISO8601-based lexical representations:
+ * YYYY
+ * ±XYYYY (where X stands for extra year digits)
+ * YYYY-MM
+ * YYYY-MM-DD
+ * YYYY-MM-DDTHH:MM:SS[.F]Z where Z is either “Z” or a time scale offset +|-HH:MM
+ *
+ * If calendar dates with reduced precision are used in a lexical representation
+ * (e.g. "2016"), then a client SHOULD interpret those dates in that reduced precision.
+ *
+ * If "type" is "TemporalRS" and "calendar" is "Gregorian", then the above lexical
+ * representation MUST be used.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","calendar","timeScale"})
+public final class TemporalRS extends CoverageJsonObject {
+
+    /**
+     * A temporal RS object MUST have a member "calendar" with value "Gregorian" or a URI.
+     * If the Gregorian calendar is used, then "calendar" MUST have the value "Gregorian" and cannot be a URI.
+     */
+    public String calendar;
+    /**
+     * A temporal RS object MAY have a member "timeScale" with a URI as value.
+     * If omitted, the time scale defaults to "UTC". If the time scale is UTC,
+     * the "timeScale" member MUST be omitted.
+     */
+    public String timeScale;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof TemporalRS)) return false;
+
+        final TemporalRS cdt = ((TemporalRS) other);
+        return super.equals(other)
+            && Objects.equals(calendar, cdt.calendar)
+            && Objects.equals(timeScale, cdt.timeScale);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                calendar,
+                timeScale);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TileSet.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TileSet.java
new file mode 100644
index 0000000000..4e626c2266
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TileSet.java
@@ -0,0 +1,75 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"tileShape","urlTemplate"})
+public final class TileSet extends Dictionary<Object> {
+
+    /**
+     * A TileSet object MUST have a member with the name "tileShape" where
+     * the value is an array of the same length as "shape" and where each
+     * array element is either null or an integer lower or equal than the
+     * corresponding element in "shape". A null value denotes that the axis
+     * is not tiled.
+     */
+    public Integer[] tileShape;
+    /**
+     * A TileSet object MUST have a member with the name "urlTemplate" where
+     * the value is a Level 1 URI template as defined in RFC 6570 .
+     * The URI template MUST contain a variable for each axis name whose
+     * corresponding element in "tileShape" is not null. A variable for an
+     * axis of total size totalSize (from "shape") and tile size tileSize
+     * (from "tileShape") has as value one of the integers 0, 1, …​, q + r - 1
+     * where q and r are the quotient and remainder obtained by dividing
+     * totalSize by tileSize. Each URI that can be generated from the URI
+     * template MUST resolve to an NdArray CoverageJSON document where the
+     * members "dataType" and "axisNames`" are identical to the ones of the
+     * TiledNdArray object, and where each value of `"shape" is an integer
+     * equal, or lower if an edge tile, to the corresponding element in
+     * "tileShape" while replacing null with the corresponding element of
+     * "shape" of the TiledNdArray.
+     */
+    public String urlTemplate;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof TileSet)) return false;
+
+        final TileSet cdt = ((TileSet) other);
+        return super.equals(other)
+            && Objects.equals(urlTemplate, cdt.urlTemplate)
+            && Arrays.equals(tileShape, cdt.tileShape);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                urlTemplate,
+                Arrays.hashCode(tileShape));
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TiledNdArray.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TiledNdArray.java
new file mode 100644
index 0000000000..43d7f710c7
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/TiledNdArray.java
@@ -0,0 +1,79 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A CoverageJSON object with the type "TiledNdArray" is a TiledNdArray object.
+ * It represents a multidimensional (>= 1D) array with named axes that is split
+ * up into sets of linked NdArray documents. Each tileset typically covers a
+ * specific data access scenario, for example, loading a single time slice of a
+ * grid vs. loading a time series of a spatial subset of a grid.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"dataType","shape","axisNames","tileSets"})
+public final class TiledNdArray extends Dictionary<Object> {
+
+    /**
+     * A TiledNdArray object MUST have a member with the name "dataType" where
+     * the value is either "float", "integer", or "string".
+     */
+    public String dataType;
+    /**
+     * A TiledNdArray object MUST have a member with the name "shape" where the
+     * value is a non-empty array of integers.
+     */
+    public int[] shape;
+    /**
+     * A TiledNdArray object MUST have a member with the name "axisNames" where
+     * the value is a string array of the same length as "shape".
+     */
+    public String[] axisNames;
+    /**
+     * A TiledNdArray object MUST have a member with the name "tileSets" where
+     * the value is a non-empty array of TileSet objects.
+     */
+    public TileSet[] tileSets;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof TiledNdArray)) return false;
+
+        final TiledNdArray cdt = ((TiledNdArray) other);
+        return super.equals(other)
+            && Objects.equals(dataType, cdt.dataType)
+            && Arrays.equals(shape, cdt.shape)
+            && Arrays.equals(axisNames, cdt.axisNames)
+            && Arrays.equals(tileSets, cdt.tileSets);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                dataType,
+                Arrays.hashCode(shape),
+                Arrays.hashCode(axisNames),
+                Arrays.hashCode(tileSets));
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Unit.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Unit.java
new file mode 100644
index 0000000000..78b94bacce
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/Unit.java
@@ -0,0 +1,79 @@
+/*
+ * 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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * A "unit" where the value is an object which MUST have either or both the members
+ * "label" or/and "symbol".
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"id","label","symbol"})
+public final class Unit extends Dictionary<Object> {
+
+    /**
+     * MAY have the member "id".
+     * If given, the value of "id" MUST be a string and
+     * SHOULD be a common identifier. It is RECOMMENDED to reference a unit
+     * serialization scheme to allow automatic unit conversion.
+     */
+    public String id;
+    /**
+     * If given, the value of "label" MUST be an i18n object of the name of
+     * the unit and SHOULD be short.
+     */
+    public I18N label;
+    /**
+     * If given, the value of "symbol" MUST either be a string of the symbolic notation of the unit,
+     * or an object with the members "value" and "type".
+     */
+    public Object symbol;
+
+    public Unit() {
+    }
+
+    public Unit(String id, I18N label, Object symbol) {
+        this.id = id;
+        this.label = label;
+        this.symbol = symbol;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof Unit)) return false;
+
+        final Unit cdt = ((Unit) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(label, cdt.label)
+            && Objects.equals(symbol, cdt.symbol);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                label,
+                symbol);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/VerticalCRS.java b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/VerticalCRS.java
new file mode 100644
index 0000000000..d3090a9670
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/java/org/apache/sis/internal/coveragejson/binding/VerticalCRS.java
@@ -0,0 +1,61 @@
+/*
+ * 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 anz "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.coveragejson.binding;
+
+import jakarta.json.bind.annotation.JsonbNillable;
+import jakarta.json.bind.annotation.JsonbPropertyOrder;
+import java.util.Objects;
+
+/**
+ * Vertical CRSs use a single coordinate to denote some measure of height or depth,
+ * usually approximately oriented with gravity.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@JsonbNillable(false)
+@JsonbPropertyOrder({"type","id","description"})
+public final class VerticalCRS extends CoverageJsonObject {
+
+    /**
+     * The object MAY have an "id" member, whose value MUST be a string and
+     * SHOULD be a common identifier for the reference system.
+     */
+    public String id;
+    /**
+     * The object MAY have a "description" member, where the value MUST be an
+     * i18n object, but no standardised content is interpreted from this description.
+     */
+    public I18N description;
+
+    @Override
+    public boolean equals(Object other) {
+        if (other == this) return true;
+        if (!(other instanceof VerticalCRS)) return false;
+
+        final VerticalCRS cdt = ((VerticalCRS) other);
+        return super.equals(other)
+            && Objects.equals(id, cdt.id)
+            && Objects.equals(description, cdt.description);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode() + Objects.hash(
+                id,
+                description);
+    }
+}
diff --git a/storage/sis-coveragejson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider b/storage/sis-coveragejson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider
new file mode 100644
index 0000000000..458f567f12
--- /dev/null
+++ b/storage/sis-coveragejson/src/main/resources/META-INF/services/org.apache.sis.storage.DataStoreProvider
@@ -0,0 +1 @@
+org.apache.sis.internal.coveragejson.CoverageJsonStoreProvider
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java
new file mode 100644
index 0000000000..23fe71e7fd
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonStoreTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.coveragejson;
+
+import jakarta.json.bind.JsonbBuilder;
+import java.awt.image.Raster;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.storage.Aggregate;
+import org.apache.sis.storage.DataStore;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.test.TestCase;
+import org.eclipse.yasson.internal.JsonBindingBuilder;
+import org.junit.Assert;
+import org.junit.Test;
+import org.opengis.metadata.spatial.DimensionNameType;
+
+/**
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class CoverageJsonStoreTest extends TestCase {
+
+    /**
+     * Test coverage example from https://covjson.org/playground/.
+     */
+    @Test
+    public void testCoverageXYZT() throws Exception {
+
+        try (final DataStore store = new CoverageJsonStoreProvider().open(new StorageConnector(CoverageJsonStoreTest.class.getResource("coverage_xyzt.json")))) {
+
+            //test grid coverage resource exist
+            Assert.assertTrue(store instanceof Aggregate);
+            final Aggregate aggregate = (Aggregate) store;
+            Assert.assertEquals(1, aggregate.components().size());
+            final Resource candidate = aggregate.components().iterator().next();
+            Assert.assertTrue(candidate instanceof GridCoverageResource);
+            final GridCoverageResource gcr = (GridCoverageResource) candidate;
+
+            JsonbBuilder jcb = new JsonBindingBuilder();
+            { //test grid geometry
+                final GridGeometry result = gcr.getGridGeometry();
+                System.out.println(result);
+
+                Assert.assertEquals(4, result.getDimension());
+
+                final GridExtent expectedExtent = new GridExtent(new DimensionNameType[]{
+                    DimensionNameType.valueOf("x"),
+                    DimensionNameType.valueOf("y"),
+                    DimensionNameType.valueOf("z"),
+                    DimensionNameType.valueOf("t")},
+                        new long[]{0,0,0,0}, new long[]{2,1,0,0}, true);
+                Assert.assertEquals(expectedExtent, result.getExtent());
+                Assert.assertEquals(CRS.compound(CommonCRS.WGS84.geographic3D(), CommonCRS.Temporal.JAVA.crs()), result.getCoordinateReferenceSystem());
+                //TODO test transform
+            }
+
+
+            {   //test data
+                GridCoverage coverage = gcr.read(null);
+                Raster data = coverage.render(null).getData();
+            }
+        }
+
+    }
+
+}
diff --git a/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonTestSuite.java b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonTestSuite.java
new file mode 100644
index 0000000000..3de67e2b7d
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/CoverageJsonTestSuite.java
@@ -0,0 +1,44 @@
+/*
+ * 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.coveragejson;
+
+import org.apache.sis.internal.coveragejson.binding.BindingTest;
+import org.apache.sis.test.TestSuite;
+import org.junit.BeforeClass;
+import org.junit.runners.Suite;
+
+
+/**
+ * All tests from the {@code sis-coveragejson} module, in rough dependency order.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+@Suite.SuiteClasses({
+    BindingTest.class,
+    CoverageJsonStoreTest.class
+})
+public final class CoverageJsonTestSuite extends TestSuite {
+    /**
+     * Verifies the list of tests before to run the suite.
+     * See {@link #verifyTestList(Class, Class[])} for more information.
+     */
+    @BeforeClass
+    public static void verifyTestList() {
+        assertNoMissingTest(CoverageJsonTestSuite.class);
+        verifyTestList(CoverageJsonTestSuite.class);
+    }
+}
diff --git a/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/binding/BindingTest.java b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/binding/BindingTest.java
new file mode 100644
index 0000000000..6455364261
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/java/org/apache/sis/internal/coveragejson/binding/BindingTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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 anz "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.coveragejson.binding;
+
+import jakarta.json.JsonObject;
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.sis.test.TestCase;
+import org.eclipse.yasson.YassonConfig;
+import org.junit.AfterClass;
+import static org.junit.Assert.*;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+/**
+ * Test coverage-json bindings.
+ *
+ * @author Johann Sorel (Geomatys)
+ */
+public class BindingTest extends TestCase {
+
+    private static final JsonbConfig CONFIG = new YassonConfig().withFormatting(true);
+
+    private static Jsonb jsonb;
+
+    public static String readResource(String path) throws IOException {
+        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+
+        int nRead;
+        byte[] data = new byte[16384];
+        try (InputStream in = BindingTest.class.getResourceAsStream(path)) {
+            while ((nRead = in.read(data, 0, data.length)) != -1) {
+              buffer.write(data, 0, nRead);
+            }
+        }
+        buffer.flush();
+        return new String(buffer.toByteArray(), StandardCharsets.UTF_8);
+    }
+
+    private void compare(String jsonpath, Object expected) throws IOException {
+        String json = readResource(jsonpath);
+        //reformat it the same way.
+        JsonObject map = jsonb.fromJson(json, JsonObject.class);
+        String formattedJson = jsonb.toJson(map);
+
+        final Object candidate = jsonb.fromJson(json, expected.getClass());
+        expected.equals(candidate);
+        assertEquals(expected, candidate);
+        assertEquals(formattedJson, jsonb.toJson(candidate));
+    }
+
+    @BeforeClass
+    public static void beforeClass() {
+        jsonb = JsonbBuilder.create(CONFIG);
+    }
+
+    @AfterClass
+    public static void afterClass() throws Exception {
+        jsonb.close();
+    }
+
+    @Test
+    public void testAxeBounds() throws Exception {
+        final Axe expected = new Axe();
+        expected.values = asList(20,21);
+        expected.bounds = asList(19.5,20.5,20.5,21.5);
+        compare("axe_bounds.json", expected);
+    }
+
+    @Test
+    public void testAxePolygon() throws Exception {
+        final Axe expected = new Axe();
+        expected.dataType = "polygon";
+        expected.coordinates = Arrays.asList("x","y");
+        expected.values = Arrays.asList(
+            Arrays.asList(
+                Arrays.asList(
+                    asList(100.0, 0.0),
+                    asList(101.0, 0.0),
+                    asList(101.0, 1.0),
+                    asList(100.0, 1.0),
+                    asList(100.0, 0.0)
+                )
+            )
+        );
+        compare("axe_polygon.json", expected);
+    }
+
+    @Test
+    public void testAxeRegular() throws Exception {
+        final Axe expected = new Axe();
+        expected.start = 0.0;
+        expected.stop = 5.0;
+        expected.num = 6;
+        compare("axe_regular.json", expected);
+    }
+
+    @Test
+    public void testAxeTuples() throws Exception {
+        final Axe expected = new Axe();
+        expected.dataType = "tuple";
+        expected.coordinates = Arrays.asList("t","x","y");
+        expected.values = Arrays.asList(
+                asList("2008-01-01T04:00:00Z",1,20),
+                asList("2008-01-01T04:30:00Z",2,21)
+        );
+        compare("axe_tuples.json", expected);
+    }
+
+    @Test
+    public void testCoverageVerticalProfile() throws Exception {
+
+        final GeographicCRS geoCrs = new GeographicCRS();
+        geoCrs.id = "http://www.opengis.net/def/crs/OGC/1.3/CRS84";
+
+        final VerticalCRS zCrs = new VerticalCRS();
+        //"cs":{"csAxes":[{"name":{"en":"Pressure"},"direction":"down","unit":{"symbol":"Pa"}}]}
+        final Map<String,Object> axe = new LinkedHashMap<>();
+        axe.put("name", Map.of("en", "Pressure"));
+        axe.put("direction", "down");
+        axe.put("unit", Map.of("symbol", "Pa"));
+        final Map<String,Object> cs = new HashMap<>();
+        cs.put("csAxes", Arrays.asList(axe));
+        //zCrs.setAnyProperty("cs", cs); //TODO undefined attributes ignored
+
+        final TemporalRS tCrs = new TemporalRS();
+        tCrs.calendar = "Gregorian";
+
+        final ReferenceSystemConnection georsc = new ReferenceSystemConnection();
+        georsc.coordinates = Arrays.asList("x", "y");
+        georsc.system = geoCrs;
+
+        final ReferenceSystemConnection zrsc = new ReferenceSystemConnection();
+        zrsc.coordinates = Arrays.asList("z");
+        zrsc.system = zCrs;
+
+        final ReferenceSystemConnection trsc = new ReferenceSystemConnection();
+        trsc.coordinates = Arrays.asList("t");
+        trsc.system = tCrs;
+
+        final Domain domain = new Domain();
+        domain.domainType = "VerticalProfile";
+        domain.axes = new Axes();
+        domain.axes.x = new Axe();
+        domain.axes.y = new Axe();
+        domain.axes.z = new Axe();
+        domain.axes.t = new Axe();
+        domain.axes.x.values = asList(-10.1);
+        domain.axes.y.values = asList(-40.2);
+        domain.axes.z.values = asList(5.4562, 8.9282);
+        domain.axes.t.values = asList("2013-01-13T11:12:20Z");
+        domain.referencing = Arrays.asList(georsc, zrsc, trsc);
+
+        final Parameter PSAL = new Parameter();
+        PSAL.description = new I18N("en", "The measured salinity, in practical salinity units (psu) of the sea water ");
+        PSAL.unit = new Unit(null,null,"psu");
+        PSAL.observedProperty = new ObservedProperty("http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/", new I18N("en", "Sea Water Salinity"), null, null);
+
+        final Parameter POTM = new Parameter();
+        POTM.description = new I18N("en", "The potential temperature, in degrees celcius, of the sea water");
+        POTM.unit = new Unit(null,null,"°C");
+        POTM.observedProperty = new ObservedProperty("http://vocab.nerc.ac.uk/standard_name/sea_water_potential_temperature/", new I18N("en", "Sea Water Potential Temperature"), null, null);
+
+        final Parameters parameters = new Parameters();
+        parameters.setAnyProperty("PSAL", PSAL);
+        parameters.setAnyProperty("POTM", POTM);
+
+        final NdArray PSALr = new NdArray();
+        PSALr.dataType ="float";
+        PSALr.shape = new int[]{2};
+        PSALr.axisNames = new String[]{"z"};
+        PSALr.values = asList(43.9599, 43.9599);
+
+        final NdArray POTMr = new NdArray();
+        POTMr.dataType ="float";
+        POTMr.shape = new int[]{2};
+        POTMr.axisNames = new String[]{"z"};
+        POTMr.values = asList(23.8, 23.7);
+
+        final Ranges ranges = new Ranges();
+        ranges.setAnyProperty("PSAL", PSALr);
+        ranges.setAnyProperty("POTM", POTMr);
+
+        final Coverage expected = new Coverage();
+        expected.domain = domain;
+        expected.parameters = parameters;
+        expected.ranges = ranges;
+
+        compare("coverage_vertical_profile_nocs.json", expected);
+    }
+
+    /**
+     * Convert numeric values to BigDecimal for equality tests.
+     */
+    private static List<Object> asList(Object... array) {
+        final List<Object> lst = new ArrayList<>(array.length);
+        for (int i=0;i<array.length;i++) {
+            if (array[i] instanceof Integer) {
+                lst.add(BigDecimal.valueOf((Integer) array[i]));
+            } else if (array[i] instanceof Double) {
+                lst.add(BigDecimal.valueOf((Double) array[i]));
+            } else {
+                lst.add(array[i]);
+            }
+        }
+        return lst;
+    }
+}
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_bounds.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_bounds.json
new file mode 100644
index 0000000000..a4d3c7c93b
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_bounds.json
@@ -0,0 +1,12 @@
+{
+  "values":[
+    20,
+    21
+  ],
+  "bounds":[
+    19.5,
+    20.5,
+    20.5,
+    21.5
+  ]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_polygon.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_polygon.json
new file mode 100644
index 0000000000..8fbaf14501
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_polygon.json
@@ -0,0 +1,33 @@
+{
+  "dataType":"polygon",
+  "coordinates":[
+    "x",
+    "y"
+  ],
+  "values":[
+    [
+      [
+        [
+          100.0,
+          0.0
+        ],
+        [
+          101.0,
+          0.0
+        ],
+        [
+          101.0,
+          1.0
+        ],
+        [
+          100.0,
+          1.0
+        ],
+        [
+          100.0,
+          0.0
+        ]
+      ]
+    ]
+  ]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_regular.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_regular.json
new file mode 100644
index 0000000000..010b46d9a9
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_regular.json
@@ -0,0 +1,5 @@
+{
+  "start": 0.0,
+  "stop": 5.0,
+  "num": 6
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_tuples.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_tuples.json
new file mode 100644
index 0000000000..79918750f3
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/axe_tuples.json
@@ -0,0 +1,20 @@
+{
+  "dataType":"tuple",
+  "coordinates":[
+    "t",
+    "x",
+    "y"
+  ],
+  "values":[
+    [
+      "2008-01-01T04:00:00Z",
+      1,
+      20
+    ],
+    [
+      "2008-01-01T04:30:00Z",
+      2,
+      21
+    ]
+  ]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile.json
new file mode 100644
index 0000000000..a975d21e4c
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile.json
@@ -0,0 +1,91 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type" : "Domain",
+    "domainType" : "VerticalProfile",
+    "axes": {
+      "x" : { "values": [-10.1] },
+      "y" : { "values": [ -40.2] },
+      "z" : { "values": [
+              5.4562, 8.9282 ] },
+      "t" : { "values": ["2013-01-13T11:12:20Z"] }
+    },
+    "referencing": [{
+      "coordinates": ["x","y"],
+      "system": {
+        "type": "GeographicCRS",
+        "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+      }
+    }, {
+      "coordinates": ["z"],
+      "system": {
+        "type": "VerticalCRS",
+        "cs": {
+          "csAxes": [{
+            "name": {
+              "en": "Pressure"
+            },
+            "direction": "down",
+            "unit": {
+              "symbol": "Pa"
+            }
+          }]
+        }
+      }
+    }, {
+      "coordinates": ["t"],
+      "system": {
+        "type": "TemporalRS",
+        "calendar": "Gregorian"
+      }
+    }]
+  },
+  "parameters" : {
+    "PSAL": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The measured salinity, in practical salinity units (psu) of the sea water "
+      },
+      "unit" : {
+        "symbol" : "psu"
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/",
+        "label" : {
+          "en": "Sea Water Salinity"
+        }
+      }
+    },
+    "POTM": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The potential temperature, in degrees celcius, of the sea water"
+      },
+      "unit" : {
+        "symbol" : "°C"
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_water_potential_temperature/",
+        "label" : {
+          "en": "Sea Water Potential Temperature"
+        }
+      }
+    }
+  },
+  "ranges" : {
+    "PSAL" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [2],
+      "values" : [ 43.9599, 43.9599 ]
+    },
+    "POTM" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [2],
+      "values" : [ 23.8, 23.7 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile_nocs.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile_nocs.json
new file mode 100644
index 0000000000..fd77ff48d6
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coverage_vertical_profile_nocs.json
@@ -0,0 +1,80 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type" : "Domain",
+    "domainType" : "VerticalProfile",
+    "axes": {
+      "x" : { "values": [-10.1] },
+      "y" : { "values": [ -40.2] },
+      "z" : { "values": [
+              5.4562, 8.9282 ] },
+      "t" : { "values": ["2013-01-13T11:12:20Z"] }
+    },
+    "referencing": [{
+      "coordinates": ["x","y"],
+      "system": {
+        "type": "GeographicCRS",
+        "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+      }
+    }, {
+      "coordinates": ["z"],
+      "system": {
+        "type": "VerticalCRS"
+      }
+    }, {
+      "coordinates": ["t"],
+      "system": {
+        "type": "TemporalRS",
+        "calendar": "Gregorian"
+      }
+    }]
+  },
+  "parameters" : {
+    "PSAL": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The measured salinity, in practical salinity units (psu) of the sea water "
+      },
+      "unit" : {
+        "symbol" : "psu"
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/",
+        "label" : {
+          "en": "Sea Water Salinity"
+        }
+      }
+    },
+    "POTM": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The potential temperature, in degrees celcius, of the sea water"
+      },
+      "unit" : {
+        "symbol" : "°C"
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_water_potential_temperature/",
+        "label" : {
+          "en": "Sea Water Potential Temperature"
+        }
+      }
+    }
+  },
+  "ranges" : {
+    "PSAL" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [2],
+      "values" : [ 43.9599, 43.9599 ]
+    },
+    "POTM" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [2],
+      "values" : [ 23.8, 23.7 ]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coveragecollection.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coveragecollection.json
new file mode 100644
index 0000000000..e1af6e39d2
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/coveragecollection.json
@@ -0,0 +1,92 @@
+{
+  "type" : "CoverageCollection",
+  "domainType" : "VerticalProfile",
+  "parameters" : {
+    "PSAL": {
+      "type" : "Parameter",
+      "description" : {
+        "en": "The measured salinity, in practical salinity units (psu) of the sea water"
+      },
+      "unit" : {
+        "symbol" : "psu"
+      },
+      "observedProperty" : {
+        "id": "http://vocab.nerc.ac.uk/standard_name/sea_water_salinity/",
+        "label" : {
+          "en": "Sea Water Salinity"
+        }
+      }
+    }
+  },
+  "referencing": [{
+    "coordinates": ["x","y"],
+    "system": {
+      "type": "GeographicCRS",
+      "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+    }
+  }, {
+    "coordinates": ["z"],
+    "system": {
+      "type": "VerticalCRS",
+      "cs": {
+        "csAxes": [{
+          "name": {
+            "en": "Pressure"
+          },
+          "direction": "down",
+          "unit": {
+            "symbol": "Pa"
+          }
+        }]
+      }
+    }
+  }, {
+    "coordinates": ["t"],
+    "system": {
+      "type": "TemporalRS",
+      "calendar": "Gregorian"
+    }
+  }],
+  "coverages": [
+    {
+      "type" : "Coverage",
+      "domain" : {
+        "type": "Domain",
+        "axes": {
+          "x": { "values": [-10.1] },
+          "y": { "values": [-40.2] },
+          "z": { "values": [ 5, 8, 14 ] },
+          "t": { "values": ["2013-01-13T11:12:20Z"] }
+        }
+      },
+      "ranges" : {
+        "PSAL" : {
+          "type" : "NdArray",
+          "dataType": "float",
+          "shape": [3],
+          "axisNames": ["z"],
+          "values" : [ 43.7, 43.8, 43.9 ]
+        }
+      }
+    }, {
+      "type" : "Coverage",
+      "domain" : {
+        "type": "Domain",
+        "axes": {
+          "x": { "values": [-11.1] },
+          "y": { "values": [-45.2] },
+          "z": { "values": [ 4, 7, 9 ] },
+          "t": { "values": ["2013-01-13T12:12:20Z"] }
+        }
+      },
+      "ranges" : {
+        "PSAL" : {
+          "type" : "NdArray",
+          "dataType": "float",
+          "shape": [3],
+          "axisNames": ["z"],
+          "values" : [ 42.7, 41.8, 40.9 ]
+        }
+      }
+    }]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_grid.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_grid.json
new file mode 100644
index 0000000000..042aed9e21
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_grid.json
@@ -0,0 +1,26 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type" : "Domain",
+    "domainType" : "Grid",
+    "axes": {
+      "x": { "values": [1,2,3] },
+      "y": { "values": [20,21] },
+      "z": { "values": [1] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": []
+  },
+  "parameters" : {
+    "temperature": {}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t", "z", "y", "x"],
+      "shape": [1, 1, 2, 3],
+      "values" : []
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_trajectory.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_trajectory.json
new file mode 100644
index 0000000000..43b421ded3
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domain_trajectory.json
@@ -0,0 +1,27 @@
+{
+  "type": "Domain",
+  "domainType": "Trajectory",
+  "axes": {
+    "composite": {
+      "dataType": "tuple",
+      "coordinates": ["t","x","y"],
+      "values": [
+        ["2008-01-01T04:00:00Z", 1, 20],
+        ["2008-01-01T04:30:00Z", 2, 21]
+      ]
+    }
+  },
+  "referencing": [{
+    "coordinates": ["t"],
+    "system": {
+      "type": "TemporalRS",
+      "calendar": "Gregorian"
+    }
+  }, {
+    "coordinates": ["x","y"],
+    "system": {
+      "type": "GeographicCRS",
+      "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+    }
+  }]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_grid.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_grid.json
new file mode 100644
index 0000000000..ee521192e1
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_grid.json
@@ -0,0 +1,5 @@
+{
+  "start": 0,
+  "stop": 5,
+  "num": 6
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipoint.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipoint.json
new file mode 100644
index 0000000000..47ca3264b5
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipoint.json
@@ -0,0 +1,30 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "MultiPoint",
+    "axes": {
+      "t": { "values": ["2008-01-01T04:00:00Z"] },
+      "composite": {
+        "dataType": "tuple",
+        "coordinates": ["x","y","z"],
+        "values": [
+          [1, 20, 1],
+          [2, 21, 3]
+        ]
+      }
+    }
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["composite"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipointseries.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipointseries.json
new file mode 100644
index 0000000000..298b8cfb1c
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipointseries.json
@@ -0,0 +1,31 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "MultiPointSeries",
+    "axes": {
+      "t": { "values": ["2008-01-01T04:00:00Z", "2008-01-01T05:00:00Z"] },
+      "composite": {
+        "dataType": "tuple",
+        "coordinates": ["x","y","z"],
+        "values": [
+          [1, 20, 1],
+          [2, 21, 3],
+          [2, 20, 4]
+        ]
+      }
+    }
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t", "composite"],
+      "shape": [2, 3],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygon.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygon.json
new file mode 100644
index 0000000000..e24f503100
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygon.json
@@ -0,0 +1,32 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "MultiPolygon",
+    "axes": {
+      "composite": {
+        "dataType": "polygon",
+        "coordinates": ["x","y"],
+        "values": [
+          [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]  ],
+          [ [ [200.0, 10.0], [201.0, 10.0], [201.0, 11.0], [200.0, 11.0], [200.0, 10.0] ] ]
+        ]
+      },
+      "z": { "values": [2] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["composite"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygonseries.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygonseries.json
new file mode 100644
index 0000000000..a3ff18886f
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_multipolygonseries.json
@@ -0,0 +1,32 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "MultiPolygonSeries",
+    "axes": {
+      "composite": {
+        "dataType": "polygon",
+        "coordinates": ["x","y"],
+        "values": [
+          [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]  ],
+          [ [ [200.0, 10.0], [201.0, 10.0], [201.0, 11.0], [200.0, 11.0], [200.0, 10.0] ] ]
+        ]
+      },
+      "z": { "values": [2] },
+      "t": { "values": ["2008-01-01T04:00:00Z", "2010-01-01T00:00:00Z", "2012-01-01T00:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t", "composite"],
+      "shape": [3, 2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_point.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_point.json
new file mode 100644
index 0000000000..ab71e6e3f4
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_point.json
@@ -0,0 +1,24 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "Point",
+    "axes": {
+      "x": { "values": [1] },
+      "y": { "values": [20] },
+      "z": { "values": [1] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_pointseries.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_pointseries.json
new file mode 100644
index 0000000000..cdb412c1c0
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_pointseries.json
@@ -0,0 +1,26 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "PointSeries",
+    "axes": {
+      "x": { "values": [1] },
+      "y": { "values": [20] },
+      "z": { "values": [1] },
+      "t": { "values": ["2008-01-01T04:00:00Z","2008-01-01T05:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygon.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygon.json
new file mode 100644
index 0000000000..7e1053008f
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygon.json
@@ -0,0 +1,29 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "Polygon",
+    "axes": {
+      "composite": {
+        "dataType": "polygon",
+        "coordinates": ["x","y"],
+        "values": [
+          [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]  ]
+        ]
+      },
+      "z": { "values": [2] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygonseries.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygonseries.json
new file mode 100644
index 0000000000..ca48a18c21
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_polygonseries.json
@@ -0,0 +1,31 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "PolygonSeries",
+    "axes": {
+      "composite": {
+        "dataType": "polygon",
+        "coordinates": ["x","y"],
+        "values": [
+          [ [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]  ]
+        ]
+      },
+      "z": { "values": [2] },
+      "t": { "values": ["2008-01-01T04:00:00Z","2008-01-01T05:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_section.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_section.json
new file mode 100644
index 0000000000..48d360f6fa
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_section.json
@@ -0,0 +1,31 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "Section",
+    "axes": {
+      "z": { "values": [10,20,30] },
+      "composite": {
+        "dataType": "tuple",
+        "coordinates": ["t","x","y"],
+        "values": [
+          ["2008-01-01T04:00:00Z", 1, 20],
+          ["2008-01-01T04:30:00Z", 2, 21]
+        ]
+      }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z", "composite"],
+      "shape": [3, 2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_trajectory.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_trajectory.json
new file mode 100644
index 0000000000..6c6937cfa0
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_trajectory.json
@@ -0,0 +1,30 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "Trajectory",
+    "axes": {
+      "composite": {
+        "dataType": "tuple",
+        "coordinates": ["t","x","y","z"],
+        "values": [
+          ["2008-01-01T04:00:00Z", 1, 20, 1],
+          ["2008-01-01T04:30:00Z", 2, 21, 3]
+        ]
+      }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["composite"],
+      "shape": [2],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_vertical_profile.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_vertical_profile.json
new file mode 100644
index 0000000000..92ff3a7adf
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/domaintype_vertical_profile.json
@@ -0,0 +1,26 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type": "Domain",
+    "domainType": "VerticalProfile",
+    "axes": {
+      "x": { "values": [1] },
+      "y": { "values": [21] },
+      "z": { "values": [1,5,20] },
+      "t": { "values": ["2008-01-01T04:00:00Z"] }
+    },
+    "referencing": [...]
+  },
+  "parameters" : {
+    "temperature": {...}
+  },
+  "ranges" : {
+    "temperature" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["z"],
+      "shape": [3],
+      "values" : [...]
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlat.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlat.json
new file mode 100644
index 0000000000..90814122a1
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlat.json
@@ -0,0 +1,4 @@
+{
+  "type": "GeographicCRS",
+  "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlatheight.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlatheight.json
new file mode 100644
index 0000000000..0c736fd05e
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/geographiccrs_longlatheight.json
@@ -0,0 +1,4 @@
+{
+  "type": "GeographicCRS",
+  "id": "http://www.opengis.net/def/crs/EPSG/0/4979"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/ndarray.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/ndarray.json
new file mode 100644
index 0000000000..da6f336406
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/ndarray.json
@@ -0,0 +1,10 @@
+{
+  "type": "NdArray",
+  "dataType": "float",
+  "shape": [4, 2],
+  "axisNames": ["y", "x"],
+  "values": [
+    12.3, 12.5, 11.5, 23.1,
+    null, null, 10.1, 9.1
+  ]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_categoricaldata.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_categoricaldata.json
new file mode 100644
index 0000000000..3afb80e7dc
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_categoricaldata.json
@@ -0,0 +1,33 @@
+{
+  "type" : "Parameter",
+  "description" : {
+    "en": "The land cover category."
+  },
+  "observedProperty" : {
+    "id" : "http://example.com/land_cover",
+    "label" : {
+      "en": "Land Cover"
+    },
+    "description" : {
+      "en": "longer description..."
+    },
+    "categories": [{
+      "id": "http://example.com/land_cover/categories/grass",
+      "label": {
+        "en": "Grass"
+      },
+      "description": {
+        "en": "Very green grass."
+      }
+    }, {
+      "id": "http://example.com/land_cover/categories/forest",
+      "label": {
+        "en": "Forest"
+      }
+    }]
+  },
+  "categoryEncoding": {
+    "http://example.com/land_cover/categories/grass": 1,
+    "http://example.com/land_cover/categories/forest": [2,3]
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_continuousdata.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_continuousdata.json
new file mode 100644
index 0000000000..2ff6c92184
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parameter_continuousdata.json
@@ -0,0 +1,24 @@
+{
+  "type" : "Parameter",
+  "description" : {
+    "en": "The sea surface temperature in degrees Celsius."
+  },
+  "observedProperty" : {
+    "id" : "http://vocab.nerc.ac.uk/standard_name/sea_surface_temperature/",
+    "label" : {
+      "en": "Sea Surface Temperature"
+    },
+    "description" : {
+      "en": "The temperature of sea water near the surface (including the part under sea-ice, if any), and not the skin temperature."
+    }
+  },
+  "unit" : {
+    "label" : {
+      "en": "Degree Celsius"
+    },
+    "symbol": {
+      "value": "Cel",
+      "type": "http://www.opengis.net/def/uom/UCUM/"
+    }
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_uncertainty.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_uncertainty.json
new file mode 100644
index 0000000000..7b871885e3
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_uncertainty.json
@@ -0,0 +1,13 @@
+{
+  "type": "ParameterGroup",
+  "label": {
+    "en": "Daily sea surface temperature with uncertainty information"
+  },
+  "observedProperty": {
+    "id": "http://vocab.nerc.ac.uk/standard_name/sea_surface_temperature/",
+    "label": {
+      "en": "Sea surface temperature"
+    }
+  },
+  "members": ["SST_mean", "SST_stddev"]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_vectorquantity.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_vectorquantity.json
new file mode 100644
index 0000000000..696eaaa3d9
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/parametergroup_vectorquantity.json
@@ -0,0 +1,9 @@
+{
+  "type": "ParameterGroup",
+  "observedProperty": {
+    "label": {
+      "en": "Wind velocity"
+    }
+  },
+  "members": ["WIND_SPEED", "WIND_DIR"]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/projectedcrs_britishnationalgrid.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/projectedcrs_britishnationalgrid.json
new file mode 100644
index 0000000000..199edd7e4e
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/projectedcrs_britishnationalgrid.json
@@ -0,0 +1,4 @@
+{
+  "type": "ProjectedCRS",
+  "id": "http://www.opengis.net/def/crs/EPSG/0/27700"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/reference_system_connection.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/reference_system_connection.json
new file mode 100644
index 0000000000..a94c3f36d5
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/reference_system_connection.json
@@ -0,0 +1,7 @@
+{
+  "coordinates": ["y","x","z"],
+  "system": {
+    "type": "GeographicCRS",
+    "id": "http://www.opengis.net/def/crs/EPSG/0/4979"
+  }
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/temporalrs.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/temporalrs.json
new file mode 100644
index 0000000000..ed5fb370a5
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/temporalrs.json
@@ -0,0 +1,4 @@
+{
+  "type": "TemporalRS",
+  "calendar": "Gregorian"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/tiledndarray.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/tiledndarray.json
new file mode 100644
index 0000000000..34dd2674da
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/tiledndarray.json
@@ -0,0 +1,16 @@
+{
+  "type" : "TiledNdArray",
+  "dataType": "integer",
+  "axisNames": ["t", "y", "x"],
+  "shape": [2, 5, 10],
+  "tileSets": [{
+    "tileShape": [null, null, null],
+    "urlTemplate": "http://example.com/a/all.covjson"
+  }, {
+    "tileShape": [1, null, null],
+    "urlTemplate": "http://example.com/b/{t}.covjson"
+  }, {
+    "tileShape": [null, 2, 3],
+    "urlTemplate": "http://example.com/c/{y}-{x}.covjson"
+  }]
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/verticalcrs_navd88.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/verticalcrs_navd88.json
new file mode 100644
index 0000000000..4bd091956f
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/binding/verticalcrs_navd88.json
@@ -0,0 +1,4 @@
+{
+  "type": "VerticalCRS",
+  "id": "http://www.opengis.net/def/crs/EPSG/0/5703"
+}
\ No newline at end of file
diff --git a/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/coverage_xyzt.json b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/coverage_xyzt.json
new file mode 100644
index 0000000000..332921893e
--- /dev/null
+++ b/storage/sis-coveragejson/src/test/resources/org/apache/sis/internal/coveragejson/coverage_xyzt.json
@@ -0,0 +1,58 @@
+{
+  "type" : "Coverage",
+  "domain" : {
+    "type" : "Domain",
+    "domainType" : "Grid",
+    "axes": {
+      "x" : { "values": [-10,-5,0] },
+      "y" : { "values": [40,50] },
+      "z" : { "values": [ 5] },
+      "t" : { "values": ["2010-01-01T00:12:20Z"] }
+    },
+    "referencing": [{
+      "coordinates": ["y","x","z"],
+      "system": {
+        "type": "GeographicCRS",
+        "id": "http://www.opengis.net/def/crs/EPSG/0/4979"
+      }
+    }, {
+      "coordinates": ["t"],
+      "system": {
+        "type": "TemporalRS",
+        "calendar": "Gregorian"
+      }
+    }]
+  },
+  "parameters" : {
+    "ICEC": {
+      "type" : "Parameter",
+      "description": {
+      	"en": "Sea Ice concentration (ice=1;no ice=0)"
+      },
+      "unit" : {
+        "label": {
+          "en": "Ratio"
+        },
+        "symbol": {
+          "value": "1",
+          "type": "http://www.opengis.net/def/uom/UCUM/"
+        }
+      },
+      "observedProperty" : {
+        "id" : "http://vocab.nerc.ac.uk/standard_name/sea_ice_area_fraction/",
+        "label" : {
+          "en": "Sea Ice Concentration"
+        }
+      }
+    }
+  },
+  "ranges" : {
+    "ICEC" : {
+      "type" : "NdArray",
+      "dataType": "float",
+      "axisNames": ["t","z","y","x"],
+      "shape": [1, 1, 2, 3],
+      "values" : [ 0.5, 0.6, 0.4, 0.6, 0.2, null ]
+    }
+  }
+}
\ No newline at end of file