diff --git a/src/utils/Doc.js b/src/utils/Doc.js
index 4eab58f7..4a22f8ed 100644
--- a/src/utils/Doc.js
+++ b/src/utils/Doc.js
@@ -169,6 +169,24 @@ export class Doc extends Observable {
     return this.get(name, YXmlFragment)
   }
 
+  /**
+   * Converts the entire document into a js object, recursively traversing each yjs type
+   * 
+   * @return {Object<string, any>}
+   */
+  toJSON () {
+    /**
+     * @type {Object<string, any>}
+     */
+    const doc = {}
+
+    for (const [k, v] of this.share.entries()) {
+      doc[k] = v.toJSON()
+    }
+    
+    return doc
+  }
+
   /**
    * Emit `destroy` event and unregister all event handlers.
    */
diff --git a/tests/index.js b/tests/index.js
index 6d019cfc..374b66f1 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -1,12 +1,13 @@
 
-import * as array from './y-array.tests.js'
+import * as doc from './y-doc.tests.js'
 import * as map from './y-map.tests.js'
+import * as array from './y-array.tests.js'
 import * as text from './y-text.tests.js'
 import * as xml from './y-xml.tests.js'
+import * as consistency from './consistency.tests.js'
 import * as encoding from './encoding.tests.js'
 import * as undoredo from './undo-redo.tests.js'
 import * as compatibility from './compatibility.tests.js'
-import * as consistency from './consistency.tests.js'
 
 import { runTests } from 'lib0/testing.js'
 import { isBrowser, isNode } from 'lib0/environment.js'
@@ -16,7 +17,7 @@ if (isBrowser) {
   log.createVConsole(document.body)
 }
 runTests({
-  map, array, text, xml, consistency, encoding, undoredo, compatibility
+  doc, map, array, text, xml, consistency, encoding, undoredo, compatibility
 }).then(success => {
   /* istanbul ignore next */
   if (isNode) {
diff --git a/tests/y-doc.tests.js b/tests/y-doc.tests.js
new file mode 100644
index 00000000..01902a43
--- /dev/null
+++ b/tests/y-doc.tests.js
@@ -0,0 +1,31 @@
+import { Doc } from './testHelper.js' // eslint-disable-line
+
+import * as Y from '../src/index.js'
+import * as t from 'lib0/testing.js'
+
+/**
+ * @param {t.TestCase} tc
+ */
+export const testToJSON = tc => {
+  const doc = new Doc()
+  t.compare(doc.toJSON(), {}, 'doc.toJSON yields empty object')
+
+  const arr = doc.getArray('array')
+  arr.push(['test1'])
+
+  const map = doc.getMap('map')
+  map.set('k1', 'v1')
+  const map2 = new Y.Map()
+  map.set('k2', map2)
+  map2.set('m2k1', 'm2v1')
+
+  t.compare(doc.toJSON(), {
+    array: ['test1'],
+    map: {
+      k1: 'v1',
+      k2: {
+        'm2k1': 'm2v1'
+      }
+    }
+  }, 'doc.toJSON has array and recursive map')
+}
\ No newline at end of file