diff --git a/data/sidebar_manual_v1200.json b/data/sidebar_manual_v1200.json
index a68a5336c..781493d78 100644
--- a/data/sidebar_manual_v1200.json
+++ b/data/sidebar_manual_v1200.json
@@ -14,6 +14,7 @@
"tuple",
"record",
"object",
+ "dict",
"variant",
"polymorphic-variant",
"null-undefined-option",
diff --git a/misc_docs/syntax/language_dict.mdx b/misc_docs/syntax/language_dict.mdx
new file mode 100644
index 000000000..73d396258
--- /dev/null
+++ b/misc_docs/syntax/language_dict.mdx
@@ -0,0 +1,51 @@
+---
+id: "dict"
+keywords: ["dict"]
+name: "dict"
+summary: "This is the `dict{}` syntax"
+category: "languageconstructs"
+---
+
+> Available in v12+
+
+The `dict{}` syntax is used to represent [dictionaries](dict.md). It's used both when creating dicts, and when pattern matching on dicts.
+
+### Example
+
+
+
+```res
+// Create a dict
+let d = dict{"A": 5, "B": 6, "C": 7}
+
+// Pattern match on the full dict
+let b = switch d {
+| dict{"B": b} => Some(b)
+| _ => None
+}
+
+// Destructure the dict
+let dict{"C": ?c} = d
+```
+
+```js
+let d = {
+ A: 5,
+ B: 6,
+ C: 7
+};
+
+let b = d.B;
+
+let b$1 = b !== undefined ? b : undefined;
+
+let c = d.C;
+```
+
+
+
+### References
+
+* [Dictionaries](/docs/manual/latest/dict)
+
+
diff --git a/pages/docs/manual/v12.0.0/dict.mdx b/pages/docs/manual/v12.0.0/dict.mdx
new file mode 100644
index 000000000..00b3f2b41
--- /dev/null
+++ b/pages/docs/manual/v12.0.0/dict.mdx
@@ -0,0 +1,161 @@
+---
+title: "Dictionary"
+description: "Dictionary data structure in ReScript"
+canonical: "/docs/manual/v12.0.0/dict"
+---
+
+# Dictionary
+
+ReScript has first class support for dictionaries. Dictionaries are mutable objects with string keys, where all values must have the same type. Dicts compile to regular JavaScript objects at runtime.
+
+## Create
+
+You can create a new dictionary in a few different ways, depending on your use case.
+
+
+
+```res prelude
+// Using the first class dict syntax
+let d = dict{"A": 5, "B": 6}
+
+// Programatically via the standard library
+let d2 = Dict.fromArray([("A", 5), ("B", 6)])
+```
+
+```js
+let d = {
+ A: 5,
+ B: 6
+};
+
+let d2 = Object.fromEntries([
+ [
+ "A",
+ 5
+ ],
+ [
+ "B",
+ 6
+ ]
+]);
+
+```
+
+
+
+A few things to note here:
+
+* Using the first class `dict{}` syntax compiles cleanly to a JavaScript object directly
+* Using `Dict.fromArray` is useful when you need to create a dictionary programatically
+
+## Access
+
+You can access values from a Dictionary either via the the standard library `Dict` module functions, or using pattern matching.
+
+
+
+```res prelude
+let d = dict{"A": 5, "B": 6, "C": 7}
+
+// Using `Dict.get`
+let a = d->Dict.get("A")
+
+// Switching on the full dict
+let b = switch d {
+| dict{"B": b} => Some(b)
+| _ => None
+}
+
+// Destructuring
+let dict{"C": ?c} = d
+```
+
+```js
+let d = {
+ A: 5,
+ B: 6,
+ C: 7
+};
+
+let a = d["A"];
+
+let b = d.B;
+
+let b$1 = b !== undefined ? b : undefined;
+
+let c = d.C;
+```
+
+
+> In the Destructuring example, we're using the `?` optional pattern match syntax to pull out the `C` key value as an optional, regardless of if the dict has it or not.
+
+## Pattern matching
+Dictionaries have first class support for pattern matching. Read more in the [dedicated guide on pattern matching and destructring in ReScript](pattern-matching-destructuring.md#match-on-dictionaries).
+
+## Updating and setting values
+
+You can set and update new values on your dictionary using the `Dict.set` function. All updates are mutable.
+
+
+
+```res prelude
+let d = dict{"A": 5, "B": 6}
+
+d->Dict.set("C", 7)
+```
+
+```js
+let d = {
+ A: 5,
+ B: 6
+};
+
+d["C"] = 7;
+```
+
+
+
+## Advanced example: Pattern matching on JSON
+
+JSON objects are represented as dictionaries (`dict`). You can leverage that fact to decode JSON in a nice way, using only language features:
+
+
+
+```res prelude
+type user = {
+ name: string,
+ email: string,
+}
+
+/** Decode JSON to a `user`.
+let decodeUser = (json: JSON.t) => {
+ switch json {
+ | Object(dict{"name": JSON.String(name), "email": JSON.String(email)}) =>
+ Some({name, email})
+ | _ => None
+ }
+}
+
+```
+
+```js
+function decodeUser(json) {
+ if (typeof json !== "object" || json === null || Array.isArray(json)) {
+ return;
+ }
+ let name = json.name;
+ if (typeof name !== "string") {
+ return;
+ }
+ let email = json.email;
+ if (typeof email === "string") {
+ return {
+ name: name,
+ email: email
+ };
+ }
+
+}
+```
+
+
diff --git a/pages/docs/manual/v12.0.0/pattern-matching-destructuring.mdx b/pages/docs/manual/v12.0.0/pattern-matching-destructuring.mdx
index b6b0ed587..3913e767d 100644
--- a/pages/docs/manual/v12.0.0/pattern-matching-destructuring.mdx
+++ b/pages/docs/manual/v12.0.0/pattern-matching-destructuring.mdx
@@ -602,6 +602,63 @@ printStudents({
+### Match on Dictionaries
+
+You can pattern match on dictionaries just like you can on other ReScript data structures.
+
+When pattern matching on a dictionary it's assumed by default that you're expecting the keys you match on to exist in the dictionary. Example:
+
+
+```res prelude
+let d = dict{"A": 5, "B": 6}
+
+// We're expecting the `B` key to exist below, and `b` will be `int` in the match branch
+let b = switch d {
+| dict{"B": b} => Some(b)
+| _ => None
+}
+```
+
+```js
+let d = {
+ A: 5,
+ B: 6
+};
+
+let b = d.B;
+
+let b$1 = b !== undefined ? b : undefined;
+```
+
+
+
+However, there are situations where you want to pull out the value of a key as an option. You can do that using the `?` optional syntax in the pattern match:
+
+
+
+```res prelude
+let d = dict{"A": 5, "B": 6}
+
+// We're pulling out `B` regardless of if it has a value or not, and therefore get `b` as `option`
+let b = switch d {
+| dict{"B": ?b} => b
+}
+```
+
+```js
+let d = {
+ A: 5,
+ B: 6
+};
+
+let b = d.B;
+```
+
+
+
+Notice how in the first case, when not using `?`, we had to supply a catch-all case `_`. That's because the pattern match _expects_ `B` to exist in the first case, for the pattern to match. If `B` doesn't exist, the match falls through to the next branch, and therefore we need to catch it to be exhaustive in our matching.
+
+However, in the second case, we don't need a catch-all case. That's because the first branch will _always_ match the dictionary - either `B` exists or it doesn't, but it doesn't matter because we're pulling it out as an optional value.
### Small Pitfall