Repository: seqsense/aws-iot-device-sdk-go
Pull request #219 (2021-02-24 02:40:10)

shadow: make state typed
Make thing state NestedState.
~Implemnt `NestedState.MapTo()` to map state (`map[string]interface{}`) to given struct.~

after #218

codecov[bot]: # [Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219?src=pr&el=h1) Report
> Merging [#219](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219?src=pr&el=desc) (57e6357) into [master](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/commit/a5d1acad17c5a1ef85e9781589ec6c522a6fda86?el=desc) (a5d1aca) will **increase** coverage by `0.59%`.
> The diff coverage is `65.69%`.

[![Impacted file tree graph](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219/graphs/tree.svg?width=650&height=150&src=pr&token=St5U6jzMqv)](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #219      +/-   ##
==========================================
+ Coverage   58.44%   59.04%   +0.59%     
==========================================
  Files          28       29       +1     
  Lines        1290     1448     +158     
==========================================
+ Hits          754      855     +101     
- Misses        470      510      +40     
- Partials       66       83      +17     
```


| [Impacted Files](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [shadow/state.go](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219/diff?src=pr&el=tree#diff-c2hhZG93L3N0YXRlLmdv) | `58.58% <45.65%> (-11.06%)` | :arrow_down: |
| [shadow/shadow.go](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219/diff?src=pr&el=tree#diff-c2hhZG93L3NoYWRvdy5nbw==) | `78.42% <54.54%> (-3.43%)` | :arrow_down: |
| [shadow/diff.go](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219/diff?src=pr&el=tree#diff-c2hhZG93L2RpZmYuZ28=) | `78.88% <78.88%> (ø)` | |
| [shadow/option.go](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219/diff?src=pr&el=tree#diff-c2hhZG93L29wdGlvbi5nbw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219?src=pr&el=footer). Last update [a5d1aca...d018312](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/pull/219?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d33c074..a323bd0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -14,9 +14,9 @@ jobs:
       fail-fast: false
       matrix:
         go-version:
-          - "1.13"
           - "1.14"
           - "1.15"
+          - "1.16"
     steps:
       - name: Checkout
         uses: actions/checkout@v2
diff --git a/README.md b/README.md
index 3fcb40d..be3f987 100644
--- a/README.md
+++ b/README.md
@@ -2,4 +2,22 @@
 
 [![Go Reference](https://pkg.go.dev/badge/godoc.org/github.com/seqsense/aws-iot-device-sdk-go.svg)](https://pkg.go.dev/godoc.org/github.com/seqsense/aws-iot-device-sdk-go) ![ci](https://github.com/seqsense/aws-iot-device-sdk-go/workflows/ci/badge.svg) [![codecov](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go/branch/master/graph/badge.svg)](https://codecov.io/gh/seqsense/aws-iot-device-sdk-go) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/seqsense/aws-iot-device-sdk-go/blob/master/LICENSE)
 
-Package awsiotdev implements AWS IoT presigned WebSockets dialer for [github.com/at-wat/mqtt-go](https://github.com/at-wat/mqtt-go), and AWS IoT functionalities.
+Package awsiotdev implements AWS IoT presigned WebSockets dialer for [github.com/at-wat/mqtt-go](https://github.com/at-wat/mqtt-go), and AWS IoT features.
+
+Implemented features:
+- Device Shadow
+- Jobs
+- Secure tunneling
+
+## Contributing
+
+Your contributions like bug reports, bug fixes, feature requests and implementing new features are welcome.
+Feel free to open a new issue and/or Pull Request.
+
+*seqsense/aws-iot-device-sdk-go* is developed under [GitHub flow](https://guides.github.com/introduction/flow/).
+Please open an issue and ask first before actually working on any significant PR (e.g. implementing features).
+Please refer to the [interface design guide](doc/interface_design_guide.md).
+
+## License
+
+This package is licensed under [Apache License Version 2.0](./LICENSE).
diff --git a/doc/interface_design_guide.md b/doc/interface_design_guide.md
new file mode 100644
index 0000000..c6e602c
--- /dev/null
+++ b/doc/interface_design_guide.md
@@ -0,0 +1,40 @@
+# Interface design guide
+
+Thank you for contributing to *seqsense/aws-iot-device-sdk-go* package!
+
+Please check out the following design guide when you implement a new feature.
+If this guide can't cover it, please discuss it on a relevant issue.
+
+## Package structure
+
+A feature is implemented as a subpackage.
+For example, AWS IoT Device Shadow is implemented as `shadow` subpackage.
+
+## Feature interface
+
+```go
+// Feature is defined as a Go interface to make it mockable.
+type Feature interface {
+  // AWS IoT feature usually subscribes to one or more MQTT topic(s).
+  // Feature interface implements `mqtt.Handler` to process MQTT messages.
+  mqtt.Handler
+
+  // If a method is not immediately processed locally and return immediately,
+  // it should be controlled by Go context.
+  Method(ctx context.Context, ...) ...
+
+  // If a method is processed locally and immediately returned,
+  // it can be without Go context.
+  ImmediateMethod(...) ...
+
+  // If an event is triggered by AWS IoT, an event handler (callback) should be
+  // set by callback setter.
+  OnEvent(func(...) ...)
+}
+
+// New creates and returns Feature interface.
+// First and second arguments should be Go context and awsiotdev.Device.
+// Last argument should be functional options.
+// Mandatory parameters which don't have default values should be passed between them.
+func New(ctx context.Context, cli awsiotdev.Device, ..., opt ...Option) (Feature, error)
+```
diff --git a/examples/go.mod b/examples/go.mod
new file mode 100644
index 0000000..ded162e
--- /dev/null
+++ b/examples/go.mod
@@ -0,0 +1,11 @@
+module github.com/seqsense/aws-iot-device-sdk-go/v5/examples
+
+go 1.16
+
+replace github.com/seqsense/aws-iot-device-sdk-go/v5 => ../
+
+require (
+	github.com/at-wat/mqtt-go v0.12.1
+	github.com/mitchellh/mapstructure v1.4.1
+	github.com/seqsense/aws-iot-device-sdk-go/v5 v5.0.0-00010101000000-000000000000
+)
diff --git a/examples/go.sum b/examples/go.sum
new file mode 100644
index 0000000..505bf62
--- /dev/null
+++ b/examples/go.sum
@@ -0,0 +1,99 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/at-wat/mqtt-go v0.12.1 h1:bO211unVuC9EN6BdkyKVislVSoIhVsXC0c57RUQlMJk=
+github.com/at-wat/mqtt-go v0.12.1/go.mod h1:b4QTk+HgoReGBw9R652AH0Kqc2OvkLSD7lOgyxWgeQE=
+github.com/aws/aws-sdk-go v1.37.15 h1:W7l7gLLMcYRlg6a+uvf3Zz4jYwdqYzhe5ymqwWoOhp4=
+github.com/aws/aws-sdk-go v1.37.15/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
+github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
+github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/examples/jobs/main.go b/examples/jobs/main.go
index 49c9ef8..a674f5b 100644
--- a/examples/jobs/main.go
+++ b/examples/jobs/main.go
@@ -77,16 +77,10 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
-	cli.Handle(mqtt.HandlerFunc(func(m *mqtt.Message) {
-		fmt.Printf("message dropped: %v\n", *m)
-	}))
 
-	if _, err := cli.Connect(ctx,
-		thingName,
-		mqtt.WithKeepAlive(30),
-	); err != nil {
-		panic(err)
-	}
+	// Multiplex message handler to route messages to multiple features.
+	var mux mqtt.ServeMux
+	cli.Handle(&mux)
 
 	j, err := jobs.New(ctx, cli)
 	if err != nil {
@@ -95,7 +89,14 @@ func main() {
 	j.OnError(func(err error) {
 		fmt.Printf("async error: %v\n", err)
 	})
-	cli.Handle(j)
+	mux.Handle("#", j) // Handle messages for Shadow.
+
+	if _, err := cli.Connect(ctx,
+		thingName,
+		mqtt.WithKeepAlive(30),
+	); err != nil {
+		panic(err)
+	}
 
 	processJob := func(jbs map[jobs.JobExecutionState][]jobs.JobExecutionSummary) {
 		if q, ok := jbs[jobs.Queued]; ok && len(q) > 0 {
diff --git a/examples/shadow/main.go b/examples/shadow/main.go
index 75a01fe..c3c2c77 100644
--- a/examples/shadow/main.go
+++ b/examples/shadow/main.go
@@ -21,6 +21,7 @@ import (
 	"time"
 
 	"github.com/at-wat/mqtt-go"
+	"github.com/mitchellh/mapstructure"
 	awsiotdev "github.com/seqsense/aws-iot-device-sdk-go/v5"
 	"github.com/seqsense/aws-iot-device-sdk-go/v5/shadow"
 )
@@ -78,16 +79,10 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
-	cli.Handle(mqtt.HandlerFunc(func(m *mqtt.Message) {
-		fmt.Printf("Message dropped: %v\n", *m)
-	}))
 
-	if _, err := cli.Connect(ctx,
-		thingName,
-		mqtt.WithKeepAlive(30),
-	); err != nil {
-		panic(err)
-	}
+	// Multiplex message handler to route messages to multiple features.
+	var mux mqtt.ServeMux
+	cli.Handle(&mux)
 
 	s, err := shadow.New(ctx, cli, shadow.WithName(shadowName))
 	if err != nil {
@@ -96,10 +91,17 @@ func main() {
 	s.OnError(func(err error) {
 		fmt.Printf("async error: %v\n", err)
 	})
-	s.OnDelta(func(delta map[string]interface{}) {
+	s.OnDelta(func(delta shadow.NestedState) {
 		fmt.Printf("delta:%s", prettyDump(delta))
 	})
-	cli.Handle(s)
+	mux.Handle("#", s) // Handle messages for Shadow.
+
+	if _, err := cli.Connect(ctx,
+		thingName,
+		mqtt.WithKeepAlive(30),
+	); err != nil {
+		panic(err)
+	}
 
 	fmt.Print("> update desire\n")
 	doc, err := s.Desire(ctx, sampleState{Value: 1, Struct: sampleStruct{Values: []int{1, 2}}})
@@ -126,6 +128,15 @@ func main() {
 	}
 	fmt.Printf("document:%s", prettyDump(doc))
 
+	// Document stores thing state as map[string]interface{}.
+	// You may want to use github.com/mitchellh/mapstructure to
+	// converts state to the given struct.
+	var typedState sampleState
+	if err := mapstructure.Decode(doc.State.Desired, &typedState); err != nil {
+		panic(err)
+	}
+	fmt.Printf("\ndocument.State.Desired (typed): %+v\n", typedState)
+
 	time.Sleep(time.Second)
 
 	fmt.Print("\n> remove one attribute from report\n")
diff --git a/examples/tunnel/main.go b/examples/tunnel/main.go
index 4daa9c0..173d2c8 100644
--- a/examples/tunnel/main.go
+++ b/examples/tunnel/main.go
@@ -80,12 +80,10 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
-	if _, err := cli.Connect(ctx,
-		thingName,
-		mqtt.WithKeepAlive(30),
-	); err != nil {
-		panic(err)
-	}
+
+	// Multiplex message handler to route messages to multiple features.
+	var mux mqtt.ServeMux
+	cli.Handle(&mux)
 
 	t, err := tunnel.New(ctx, cli, map[string]tunnel.Dialer{
 		"ssh": func() (io.ReadWriteCloser, error) {
@@ -95,7 +93,14 @@ func main() {
 	if err != nil {
 		panic(err)
 	}
-	cli.Handle(t)
+	mux.Handle("#", t) // Handle messages for Shadow.
+
+	if _, err := cli.Connect(ctx,
+		thingName,
+		mqtt.WithKeepAlive(30),
+	); err != nil {
+		panic(err)
+	}
 
 	select {}
 }
diff --git a/go.mod b/go.mod
index 5399c67..647a29b 100644
--- a/go.mod
+++ b/go.mod
@@ -2,7 +2,7 @@ module github.com/seqsense/aws-iot-device-sdk-go/v5
 
 require (
 	github.com/at-wat/mqtt-go v0.12.1
-	github.com/aws/aws-sdk-go v1.37.15
+	github.com/aws/aws-sdk-go v1.37.20
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/golang/protobuf v1.4.3
 	github.com/google/uuid v1.2.0
diff --git a/go.sum b/go.sum
index f1b511d..743c351 100644
--- a/go.sum
+++ b/go.sum
@@ -2,8 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/at-wat/mqtt-go v0.12.1 h1:bO211unVuC9EN6BdkyKVislVSoIhVsXC0c57RUQlMJk=
 github.com/at-wat/mqtt-go v0.12.1/go.mod h1:b4QTk+HgoReGBw9R652AH0Kqc2OvkLSD7lOgyxWgeQE=
-github.com/aws/aws-sdk-go v1.37.15 h1:W7l7gLLMcYRlg6a+uvf3Zz4jYwdqYzhe5ymqwWoOhp4=
-github.com/aws/aws-sdk-go v1.37.15/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
+github.com/aws/aws-sdk-go v1.37.20 h1:CJCXpMYmBJrRH8YwoSE0oB9S3J5ax+62F14sYlDCztg=
+github.com/aws/aws-sdk-go v1.37.20/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/shadow/diff.go b/shadow/diff.go
index bf47517..69bd0a5 100644
--- a/shadow/diff.go
+++ b/shadow/diff.go
@@ -21,9 +21,6 @@ import (
 
 var errInvalidAttribute = errors.New("invalid attribute key")
 
-// ErrUnsupportedMapKeyType is returned if map key type is not string.
-var ErrUnsupportedMapKeyType = errors.New("unsupported map key type")
-
 func stateDiff(base, in interface{}) (interface{}, bool, error) {
 	keys, hasChild, err := attributeKeys(base)
 	if err != nil {
@@ -170,9 +167,7 @@ func attributeByKeyImpl(v reflect.Value, k string) (reflect.Value, error) {
 			return reflect.Value{}, nil
 		}
 		return val, nil
-	case reflect.Ptr:
-		return attributeByKeyImpl(v.Elem(), k)
-	case reflect.Interface:
+	case reflect.Ptr, reflect.Interface:
 		return attributeByKeyImpl(v.Elem(), k)
 	}
 	return reflect.Value{}, errInvalidAttribute
diff --git a/shadow/shadow.go b/shadow/shadow.go
index cb5f000..4c2727a 100644
--- a/shadow/shadow.go
+++ b/shadow/shadow.go
@@ -42,7 +42,7 @@ type Shadow interface {
 	// Delete thing shadow.
 	Delete(ctx context.Context) error
 	// OnDelta sets handler of state deltas.
-	OnDelta(func(delta map[string]interface{}))
+	OnDelta(func(delta NestedState))
 	// OnError sets handler of asynchronous errors.
 	OnError(func(error))
 }
@@ -60,7 +60,7 @@ type shadow struct {
 	opts      *Options
 	doc       *ThingDocument
 	mu        sync.Mutex
-	onDelta   func(delta map[string]interface{})
+	onDelta   func(delta NestedState)
 	onError   func(err error)
 
 	chResps map[string]chan interface{}
@@ -180,14 +180,14 @@ func New(ctx context.Context, cli awsiotdev.Device, opt ...Option) (Shadow, erro
 		opts:      &opts,
 		doc: &ThingDocument{
 			State: ThingState{
-				Desired:  map[string]interface{}{},
-				Reported: map[string]interface{}{},
-				Delta:    map[string]interface{}{},
+				Desired:  NestedState{},
+				Reported: NestedState{},
+				Delta:    NestedState{},
 			},
 			Metadata: ThingStateMetadata{
-				Desired:  map[string]interface{}{},
-				Reported: map[string]interface{}{},
-				Delta:    map[string]interface{}{},
+				Desired:  NestedMetadata{},
+				Reported: NestedMetadata{},
+				Delta:    NestedMetadata{},
 			},
 		},
 
@@ -446,13 +446,13 @@ func (s *shadow) Document() *ThingDocument {
 	return s.doc.clone()
 }
 
-func (s *shadow) OnDelta(cb func(delta map[string]interface{})) {
+func (s *shadow) OnDelta(cb func(delta NestedState)) {
 	s.mu.Lock()
 	s.onDelta = cb
 	s.mu.Unlock()
 }
 
-func (s *shadow) handleDelta(delta map[string]interface{}) {
+func (s *shadow) handleDelta(delta NestedState) {
 	s.mu.Lock()
 	cb := s.onDelta
 	s.mu.Unlock()
diff --git a/shadow/state.go b/shadow/state.go
index 25dd9f9..2b31433 100644
--- a/shadow/state.go
+++ b/shadow/state.go
@@ -25,6 +25,12 @@ import (
 // ErrVersionConflict means thing state update was aborted due to version conflict.
 var ErrVersionConflict = errors.New("version conflict")
 
+// ErrUnsupportedMapKeyType is returned if map key type is not string.
+var ErrUnsupportedMapKeyType = errors.New("unsupported map key type")
+
+// ErrIncompatibleMapping is returned if state can't be converted into struct.
+var ErrIncompatibleMapping = errors.New("unable to convert state to struct")
+
 type simpleRequest struct {
 	ClientToken string `json:"clientToken"`
 }
@@ -44,9 +50,9 @@ func (e *ErrorResponse) Error() string {
 
 // ThingState represents Thing Shadow State.
 type ThingState struct {
-	Desired  map[string]interface{} `json:"desired,omitempty"`
-	Reported map[string]interface{} `json:"reported,omitempty"`
-	Delta    map[string]interface{} `json:"delta,omitempty"`
+	Desired  NestedState `json:"desired,omitempty"`
+	Reported NestedState `json:"reported,omitempty"`
+	Delta    NestedState `json:"delta,omitempty"`
 }
 
 // ThingDocument represents Thing Shadow Document.
@@ -74,10 +80,10 @@ type thingDocumentRaw struct {
 }
 
 type thingDelta struct {
-	State     map[string]interface{} `json:"state"`
-	Metadata  NestedMetadata         `json:"metadata"`
-	Version   int                    `json:"version,omitempty"`
-	Timestamp int                    `json:"timestamp,omitempty"`
+	State     NestedState    `json:"state"`
+	Metadata  NestedMetadata `json:"metadata"`
+	Version   int            `json:"version,omitempty"`
+	Timestamp int            `json:"timestamp,omitempty"`
 }
 
 func (s *ThingDocument) update(state *thingDocumentRaw) error {
@@ -127,18 +133,18 @@ func updateStateRawCommon(state map[string]interface{}, update json.RawMessage)
 	return false
 }
 
-func updateStateRaw(state map[string]interface{}, update json.RawMessage) error {
+func updateStateRaw(state NestedState, update json.RawMessage) error {
 	if updateStateRawCommon(state, update) {
 		return nil
 	}
-	var u map[string]interface{}
+	var u NestedState
 	if err := json.Unmarshal([]byte(update), &u); err != nil {
 		return ioterr.New(err, "unmarshaling update")
 	}
 	return updateState(state, u)
 }
 
-func updateStateMetadataRaw(state map[string]interface{}, update json.RawMessage) error {
+func updateStateMetadataRaw(state NestedMetadata, update json.RawMessage) error {
 	if updateStateRawCommon(state, update) {
 		return nil
 	}
@@ -158,8 +164,8 @@ func updateState(state map[string]interface{}, update map[string]interface{}) er
 	}
 	for key, val := range update {
 		switch v := val.(type) {
-		case map[string]interface{}:
-			if s, ok := state[key].(map[string]interface{}); ok {
+		case NestedState:
+			if s, ok := state[key].(NestedState); ok {
 				if err := updateState(s, v); err != nil {
 					return ioterr.New(err, "updating state")
 				}
@@ -180,3 +186,55 @@ func updateState(state map[string]interface{}, update map[string]interface{}) er
 func hasUpdate(s json.RawMessage) bool {
 	return len(s) != 0
 }
+
+// NestedState is JSON unmarshaller for state metadata.
+type NestedState map[string]interface{}
+
+func (n *NestedState) UnmarshalJSON(b []byte) error {
+	if *n == nil {
+		*n = make(map[string]interface{})
+	}
+	j := make(map[string]json.RawMessage)
+	if err := json.Unmarshal(b, &j); err != nil {
+		return err
+	}
+	for k, v := range j {
+		v2, err := unmarshalStateImpl(v)
+		if err != nil {
+			return err
+		}
+		(*n)[k] = v2
+	}
+	return nil
+}
+
+func unmarshalStateImpl(b []byte) (interface{}, error) {
+	switch {
+	case b[0] == '[' && b[len(b)-1] == ']':
+		var v2 []json.RawMessage
+		if err := json.Unmarshal(b, &v2); err != nil {
+			return nil, err
+		}
+		var v5 []interface{}
+		for _, v3 := range v2 {
+			v4, err := unmarshalStateImpl(v3)
+			if err != nil {
+				return nil, err
+			}
+			v5 = append(v5, v4)
+		}
+		return v5, nil
+	case b[0] == '{':
+		var v2 NestedState
+		if err := json.Unmarshal(b, &v2); err != nil {
+			return nil, err
+		}
+		return v2, nil
+	default:
+		var v2 interface{}
+		if err := json.Unmarshal(b, &v2); err != nil {
+			return nil, err
+		}
+		return v2, nil
+	}
+}


Test patch:
diff --git a/shadow/shadow_test.go b/shadow/shadow_test.go
index cb8a8a8..9aae7a9 100644
--- a/shadow/shadow_test.go
+++ b/shadow/shadow_test.go
@@ -103,12 +103,12 @@ func TestHandlers(t *testing.T) {
 		s := &shadow{
 			doc: &ThingDocument{
 				State: ThingState{
-					Desired: map[string]interface{}{"key": "value_init"},
+					Desired: NestedState{"key": "value_init"},
 				},
 				Version:   2,
 				Timestamp: 12345,
 			},
-			onDelta: func(delta map[string]interface{}) {
+			onDelta: func(delta NestedState) {
 				t.Error("onDelta must not be called on rejected")
 			},
 			onError: func(err error) {
@@ -117,7 +117,7 @@ func TestHandlers(t *testing.T) {
 		}
 		expectedDoc := &ThingDocument{
 			State: ThingState{
-				Desired: map[string]interface{}{"key": "value_init"},
+				Desired: NestedState{"key": "value_init"},
 			},
 			Version:   2,
 			Timestamp: 12345,
@@ -141,16 +141,16 @@ func TestHandlers(t *testing.T) {
 	t.Run("Accepted", func(t *testing.T) {
 		t.Run("Get", func(t *testing.T) {
 			chErr := make(chan error, 1)
-			chDelta := make(chan map[string]interface{}, 1)
+			chDelta := make(chan NestedState, 1)
 			s := &shadow{
 				doc: &ThingDocument{
 					State: ThingState{
-						Desired: map[string]interface{}{"key": "value_init"},
+						Desired: NestedState{"key": "value_init"},
 					},
 					Version:   2,
 					Timestamp: 12345,
 				},
-				onDelta: func(delta map[string]interface{}) {
+				onDelta: func(delta NestedState) {
 					chDelta <- delta
 				},
 				onError: func(err error) {
@@ -159,8 +159,8 @@ func TestHandlers(t *testing.T) {
 			}
 			expectedDoc := &ThingDocument{
 				State: ThingState{
-					Desired: map[string]interface{}{"key2": "value2"},
-					Delta:   map[string]interface{}{"key2": "value2"},
+					Desired: NestedState{"key2": "value2"},
+					Delta:   NestedState{"key2": "value2"},
 				},
 				Version:   3,
 				Timestamp: 12346,
@@ -199,12 +199,12 @@ func TestHandlers(t *testing.T) {
 			s := &shadow{
 				doc: &ThingDocument{
 					State: ThingState{
-						Desired: map[string]interface{}{"key": "value_init"},
+						Desired: NestedState{"key": "value_init"},
 					},
 					Version:   2,
 					Timestamp: 12345,
 				},
-				onDelta: func(delta map[string]interface{}) {
+				onDelta: func(delta NestedState) {
 					t.Error("Delete must not trigger onDelta")
 				},
 				onError: func(err error) {
@@ -227,16 +227,16 @@ func TestHandlers(t *testing.T) {
 	t.Run("Update", func(t *testing.T) {
 		t.Run("Delta", func(t *testing.T) {
 			chErr := make(chan error, 1)
-			chDelta := make(chan map[string]interface{}, 1)
+			chDelta := make(chan NestedState, 1)
 			s := &shadow{
 				doc: &ThingDocument{
 					State: ThingState{
-						Desired: map[string]interface{}{"key2": "value2"},
+						Desired: NestedState{"key2": "value2"},
 					},
 					Version:   2,
 					Timestamp: 12345,
 				},
-				onDelta: func(delta map[string]interface{}) {
+				onDelta: func(delta NestedState) {
 					chDelta <- delta
 				},
 				onError: func(err error) {
@@ -245,8 +245,8 @@ func TestHandlers(t *testing.T) {
 			}
 			expectedDoc := &ThingDocument{
 				State: ThingState{
-					Desired: map[string]interface{}{"key2": "value2"},
-					Delta:   map[string]interface{}{"key2": "value2"},
+					Desired: NestedState{"key2": "value2"},
+					Delta:   NestedState{"key2": "value2"},
 				},
 				Version:   3,
 				Timestamp: 12346,
@@ -281,16 +281,16 @@ func TestHandlers(t *testing.T) {
 		})
 		t.Run("Accepted", func(t *testing.T) {
 			chErr := make(chan error, 1)
-			chDelta := make(chan map[string]interface{}, 1)
+			chDelta := make(chan NestedState, 1)
 			s := &shadow{
 				doc: &ThingDocument{
 					State: ThingState{
-						Reported: map[string]interface{}{"key1": "value1"},
+						Reported: NestedState{"key1": "value1"},
 					},
 					Version:   2,
 					Timestamp: 12345,
 				},
-				onDelta: func(delta map[string]interface{}) {
+				onDelta: func(delta NestedState) {
 					chDelta <- delta
 				},
 				onError: func(err error) {
@@ -299,7 +299,7 @@ func TestHandlers(t *testing.T) {
 			}
 			expectedDoc := &ThingDocument{
 				State: ThingState{
-					Reported: map[string]interface{}{
+					Reported: NestedState{
 						"key1": "value1",
 						"key2": "value2",
 					},
@@ -336,12 +336,12 @@ func TestHandlers(t *testing.T) {
 			s := &shadow{
 				doc: &ThingDocument{
 					State: ThingState{
-						Desired: map[string]interface{}{"key2": "value2"},
+						Desired: NestedState{"key2": "value2"},
 					},
 					Version:   2,
 					Timestamp: 12345,
 				},
-				onDelta: func(delta map[string]interface{}) {
+				onDelta: func(delta NestedState) {
 					t.Error("Old delta must be discarded")
 				},
 				onError: func(err error) {
@@ -350,7 +350,7 @@ func TestHandlers(t *testing.T) {
 			}
 			expectedDoc := &ThingDocument{
 				State: ThingState{
-					Desired: map[string]interface{}{"key2": "value2"},
+					Desired: NestedState{"key2": "value2"},
 				},
 				Version:   2,
 				Timestamp: 12345,
@@ -429,12 +429,12 @@ func TestOnDelta(t *testing.T) {
 	}
 	cli.Handle(s)
 
-	expected := map[string]interface{}{
+	expected := NestedState{
 		"key": "value",
 	}
 
 	done := make(chan struct{})
-	s.OnDelta(func(delta map[string]interface{}) {
+	s.OnDelta(func(delta NestedState) {
 		if !reflect.DeepEqual(expected, delta) {
 			t.Fatalf("Expected delta:\n%+v\ngot:\n%+v", expected, delta)
 		}
@@ -442,7 +442,7 @@ func TestOnDelta(t *testing.T) {
 	})
 
 	req := &thingDelta{
-		State: map[string]interface{}{
+		State: NestedState{
 			"key": "value",
 		},
 		Version: 10,
@@ -475,31 +475,31 @@ func TestGet(t *testing.T) {
 			initial: &ThingDocument{
 				Version: 5,
 				State: ThingState{
-					Delta: map[string]interface{}{"key": "value2"},
+					Delta: NestedState{"key": "value2"},
 				},
 				Metadata: ThingStateMetadata{
-					Delta: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+					Delta: NestedMetadata{"key": Metadata{Timestamp: 1234}},
 				},
 			},
 			response: &ThingDocument{
 				Version: 5,
 				State: ThingState{
-					Desired:  map[string]interface{}{"key": "value2"},
-					Reported: map[string]interface{}{"key": "value"},
-					Delta:    map[string]interface{}{"key": "value2"},
+					Desired:  NestedState{"key": "value2"},
+					Reported: NestedState{"key": "value"},
+					Delta:    NestedState{"key": "value2"},
 				},
 				Metadata: ThingStateMetadata{
-					Desired:  map[string]interface{}{"key": Metadata{Timestamp: 1234}},
-					Reported: map[string]interface{}{"key": Metadata{Timestamp: 1235}},
+					Desired:  NestedMetadata{"key": Metadata{Timestamp: 1234}},
+					Reported: NestedMetadata{"key": Metadata{Timestamp: 1235}},
 				},
 			},
 			responseTopic: "get/accepted",
 			expected: &ThingDocument{
 				Version: 5,
 				State: ThingState{
-					Desired:  map[string]interface{}{"key": "value2"},
-					Reported: map[string]interface{}{"key": "value"},
-					Delta:    map[string]interface{}{"key": "value2"},
+					Desired:  NestedState{"key": "value2"},
+					Reported: NestedState{"key": "value"},
+					Delta:    NestedState{"key": "value2"},
 				},
 				Metadata: ThingStateMetadata{
 					Desired:  NestedMetadata{"key": Metadata{Timestamp: 1234}},
@@ -583,30 +583,30 @@ func TestDesire(t *testing.T) {
 		inc := inc
 		t.Run(name, func(t *testing.T) {
 			testCases := map[string]struct {
-				input         map[string]interface{}
+				input         NestedState
 				response      interface{}
 				responseTopic string
 				expected      interface{}
 				err           error
 			}{
 				"Success": {
-					input: map[string]interface{}{"key": "value"},
+					input: NestedState{"key": "value"},
 					response: &ThingDocument{
 						Version: 5,
 						State: ThingState{
-							Desired: map[string]interface{}{"key": "value"},
+							Desired: NestedState{"key": "value"},
 						},
 						Metadata: ThingStateMetadata{
-							Desired: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+							Desired: NestedMetadata{"key": Metadata{Timestamp: 1234}},
 						},
 					},
 					responseTopic: "update/accepted",
 					expected: &ThingDocument{
 						Version: 5,
 						State: ThingState{
-							Desired:  map[string]interface{}{"key": "value"},
-							Reported: map[string]interface{}{},
-							Delta:    map[string]interface{}{},
+							Desired:  NestedState{"key": "value"},
+							Reported: NestedState{},
+							Delta:    NestedState{},
 						},
 						Metadata: ThingStateMetadata{
 							Desired:  NestedMetadata{"key": Metadata{Timestamp: 1234}},
@@ -616,7 +616,7 @@ func TestDesire(t *testing.T) {
 					},
 				},
 				"Error": {
-					input: map[string]interface{}{"key": "value"},
+					input: NestedState{"key": "value"},
 					response: &ErrorResponse{
 						Code:    400,
 						Message: "Reason",
@@ -690,30 +690,30 @@ func TestReport(t *testing.T) {
 		inc := inc
 		t.Run(name, func(t *testing.T) {
 			testCases := map[string]struct {
-				input         map[string]interface{}
+				input         NestedState
 				response      interface{}
 				responseTopic string
 				expected      interface{}
 				err           error
 			}{
 				"Success": {
-					input: map[string]interface{}{"key": "value"},
+					input: NestedState{"key": "value"},
 					response: &ThingDocument{
 						Version: 5,
 						State: ThingState{
-							Reported: map[string]interface{}{"key": "value"},
+							Reported: NestedState{"key": "value"},
 						},
 						Metadata: ThingStateMetadata{
-							Reported: map[string]interface{}{"key": Metadata{Timestamp: 1234}},
+							Reported: NestedMetadata{"key": Metadata{Timestamp: 1234}},
 						},
 					},
 					responseTopic: "update/accepted",
 					expected: &ThingDocument{
 						Version: 5,
 						State: ThingState{
-							Reported: map[string]interface{}{"key": "value"},
-							Desired:  map[string]interface{}{},
-							Delta:    map[string]interface{}{},
+							Reported: NestedState{"key": "value"},
+							Desired:  NestedState{},
+							Delta:    NestedState{},
 						},
 						Metadata: ThingStateMetadata{
 							Reported: NestedMetadata{"key": Metadata{Timestamp: 1234}},
@@ -723,7 +723,7 @@ func TestReport(t *testing.T) {
 					},
 				},
 				"Error": {
-					input: map[string]interface{}{"key": "value"},
+					input: NestedState{"key": "value"},
 					response: &ErrorResponse{
 						Code:    400,
 						Message: "Reason",
diff --git a/shadow/state_test.go b/shadow/state_test.go
index 7518b93..2c22ec6 100644
--- a/shadow/state_test.go
+++ b/shadow/state_test.go
@@ -26,7 +26,7 @@ import (
 
 func TestThingDocument_update(t *testing.T) {
 	doc := &ThingDocument{
-		State:     ThingState{Desired: map[string]interface{}{"key": "value_init"}},
+		State:     ThingState{Desired: NestedState{"key": "value_init"}},
 		Version:   2,
 		Timestamp: 12345,
 	}
@@ -40,7 +40,7 @@ func TestThingDocument_update(t *testing.T) {
 	}
 
 	expected0 := &ThingDocument{
-		State:     ThingState{Desired: map[string]interface{}{"key": "value_init"}},
+		State:     ThingState{Desired: NestedState{"key": "value_init"}},
 		Version:   2,
 		Timestamp: 12345,
 	}
@@ -60,7 +60,7 @@ func TestThingDocument_update(t *testing.T) {
 	}
 
 	expected1 := &ThingDocument{
-		State:     ThingState{Desired: map[string]interface{}{"key": "new value"}},
+		State:     ThingState{Desired: NestedState{"key": "new value"}},
 		Version:   4,
 		Timestamp: 12346,
 	}
@@ -80,7 +80,7 @@ func TestThingDocument_update(t *testing.T) {
 	}
 
 	expected2 := &ThingDocument{
-		State:     ThingState{Desired: map[string]interface{}{}},
+		State:     ThingState{Desired: NestedState{}},
 		Version:   5,
 		Timestamp: 12347,
 	}
@@ -101,7 +101,7 @@ func TestThingDocument_update(t *testing.T) {
 
 	expected3 := &ThingDocument{
 		State: ThingState{
-			Desired: map[string]interface{}{"key": map[string]interface{}{"key": "value"}},
+			Desired: NestedState{"key": NestedState{"key": "value"}},
 		},
 		Version:   6,
 		Timestamp: 12348,