diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index da98ab06ec03b2c316204f7ba6cc6f6f0b0699a4..b32cc427923ba836c1a91b126d2d0022f8731ba4 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -154,6 +154,7 @@ jobs:
           yarn prettier
           yarn markdown-lint
           yarn git-check
+          yarn doc-fence-check
 
       - name: Test schema
         run: yarn test-schema
diff --git a/docs/development/configuration.md b/docs/development/configuration.md
index 333a9b4de407c2c8596d07971e5d6732ce2e06f8..d3bfca3b7f6edd4dafcdbedea4b5cbfe61a9aeb3 100644
--- a/docs/development/configuration.md
+++ b/docs/development/configuration.md
@@ -53,11 +53,10 @@ If you add a `renovate.json` file to the root of your repository, you can use th
 If you add configuration options to your `package.json` then these will override any other settings above.
 
 ```json
-"renovate": {
-  "labels": [
-    "upgrade",
-    "bot"
-  ]
+{
+  "renovate": {
+    "labels": ["upgrade", "bot"]
+  }
 }
 ```
 
diff --git a/docs/usage/config-presets.md b/docs/usage/config-presets.md
index f1e5d450e3741dc5d0d44390eb7d2fbd36d94d95..3fa8da053fc23e6ad47d4a5ec7f4cc396d59f405 100644
--- a/docs/usage/config-presets.md
+++ b/docs/usage/config-presets.md
@@ -128,28 +128,24 @@ You can find the Renovate team's preset configs at the "Config Presets" section
 If you browse the "default" presets, you will see some that contain parameters, e.g.:
 
 ```json
-    "labels": {
-      "description": "Apply labels <code>{{arg0}}</code> and <code>{{arg1}}</code> to PRs",
-      "labels": [
-        "{{arg0}}",
-        "{{arg1}}"
-      ]
-    },
-    "assignee": {
-      "description": "Assign PRs to <code>{{arg0}}</code>",
-      "assignees": [
-        "{{arg0}}"
-      ]
-    },
+{
+  "labels": {
+    "description": "Apply labels <code>{{arg0}}</code> and <code>{{arg1}}</code> to PRs",
+    "labels": ["{{arg0}}", "{{arg1}}"]
+  },
+  "assignee": {
+    "description": "Assign PRs to <code>{{arg0}}</code>",
+    "assignees": ["{{arg0}}"]
+  }
+}
 ```
 
 Here is how you would use these in your Renovate config:
 
 ```json
-  "extends": [
-    ":labels(dependencies,devops)",
-    ":assignee(rarkins)"
-  ]
+{
+  "extends": [":labels(dependencies,devops)", ":assignee(rarkins)"]
+}
 ```
 
 In short, the number of `{{argx}}` parameters in the definition is how many parameters you need to provide.
@@ -170,7 +166,9 @@ To host your preset config on GitHub:
 - In other repos, reference it in an extends array like "github>owner/name", for example:
 
 ```json
+{
   "extends": ["github>rarkins/renovate-config"]
+}
 ```
 
 From then on Renovate will use the Renovate config from the preset repo's default branch.
@@ -259,7 +257,6 @@ For example:
 {
   "name": "renovate-config-fastcore",
   "version": "0.0.1",
-  ...
   "renovate-config": {
     "default": {
       "extends": ["config:base", "schedule:nonOfficeHours"]
@@ -271,7 +268,9 @@ For example:
 Then in each of your repositories you can add your Renovate config like:
 
 ```json
+{
   "extends": ["fastcore"]
+}
 ```
 
 Any repository including this config will then adopt the rules of the default `library` preset but schedule it on weeknights or weekends.
@@ -279,5 +278,7 @@ Any repository including this config will then adopt the rules of the default `l
 Note: if you prefer to publish using the namespace `@fastcore/renovate-config` then you would use the `@` prefix instead:
 
 ```json
+{
   "extends": ["@fastcore"]
+}
 ```
diff --git a/docs/usage/docker.md b/docs/usage/docker.md
index 6fedd98948010d1844758dbf4fb34cb58c3d3d6d..75be256e8ccb23ee523013518d3350c5382321a3 100644
--- a/docs/usage/docker.md
+++ b/docs/usage/docker.md
@@ -126,11 +126,13 @@ If you wish to override Docker settings for one particular type of manager, use
 For example, to disable digest updates for Docker Compose only but leave them for other managers like `Dockerfile`, you would use this:
 
 ```json
+{
   "docker-compose": {
     "digest": {
       "enabled": false
     }
   }
+}
 ```
 
 The following configuration options are applicable to Docker:
diff --git a/docs/usage/faq.md b/docs/usage/faq.md
index d40443ca7bf35913de8896a1e90611aeadfb8900..a1b219af4fd99bdf6d43252defd9d2c9a042ac3c 100644
--- a/docs/usage/faq.md
+++ b/docs/usage/faq.md
@@ -180,12 +180,14 @@ Set the configuration option `labels` to an array of labels to use.
 e.g.
 
 ```json
-"packageRules": [
-  {
-    "matchPackageNames": ["abc"],
-    "assignees": ["importantreviewer"]
-  }
-]
+{
+  "packageRules": [
+    {
+      "matchPackageNames": ["abc"],
+      "assignees": ["importantreviewer"]
+    }
+  ]
+}
 ```
 
 ### Apply a rule, but only for packages starting with `abc`
@@ -193,12 +195,14 @@ e.g.
 Do the same as above, but instead of using `matchPackageNames`, use `matchPackagePatterns` and a regex:
 
 ```json
-"packageRules": [
-  {
-    "matchPackagePatterns": "^abc",
-    "assignees": ["importantreviewer"]
-  }
-]
+{
+  "packageRules": [
+    {
+      "matchPackagePatterns": "^abc",
+      "assignees": ["importantreviewer"]
+    }
+  ]
+}
 ```
 
 ### Group all packages starting with `abc` together in one PR
@@ -206,12 +210,14 @@ Do the same as above, but instead of using `matchPackageNames`, use `matchPackag
 As above, but apply a `groupName`:
 
 ```json
-"packageRules": [
-  {
-    "matchPackagePatterns": "^abc",
-    "groupName": ["abc packages"]
-  }
-]
+{
+  "packageRules": [
+    {
+      "matchPackagePatterns": "^abc",
+      "groupName": ["abc packages"]
+    }
+  ]
+}
 ```
 
 ### Change the default values for branch name, commit message, PR title or PR description
diff --git a/docs/usage/golang.md b/docs/usage/golang.md
index 933956a60574e4eae8bc297c479bd01d52beff2d..67f9b8c5dac9a883176bc14695126bb8871b1aef 100644
--- a/docs/usage/golang.md
+++ b/docs/usage/golang.md
@@ -46,9 +46,11 @@ You can force Renovate to use a specific version of Go by setting a constraint.
 As an example, say you want Renovate to use the latest patch version of the `1.16` Go binary, you'd put this in your Renovate config:
 
 ```json
+{
   "constraints": {
     "go": "1.16"
   }
+}
 ```
 
 We do not support patch level versions for the minimum `go` version.
diff --git a/docs/usage/noise-reduction.md b/docs/usage/noise-reduction.md
index a44270a4c34707f6327607ca040b071cf12c7a4e..ec2cf2a67007fd2e390622461d918623a7bfc94c 100644
--- a/docs/usage/noise-reduction.md
+++ b/docs/usage/noise-reduction.md
@@ -28,12 +28,14 @@ You may wish to take this further, for example you might want to group together
 In that case you might create a config like this:
 
 ```json
+{
   "packageRules": [
     {
-      "matchPackagePatterns": [ "eslint" ],
+      "matchPackagePatterns": ["eslint"],
       "groupName": "eslint"
     }
   ]
+}
 ```
 
 By setting `matchPackagePatterns` to "eslint", it means that any package with ESLint anywhere in its name will be grouped into a `renovate/eslint` branch and related PR.
@@ -79,25 +81,29 @@ If you think about it, updates to `eslint` rules don't exactly need to be applie
 You don't want to get too far behind, so how about we update `eslint` packages only once a month?
 
 ```json
+{
   "packageRules": [
     {
-      "matchPackagePatterns": [ "eslint" ],
+      "matchPackagePatterns": ["eslint"],
       "groupName": "eslint",
       "schedule": ["on the first day of the month"]
     }
   ]
+}
 ```
 
 Or perhaps at least weekly:
 
 ```json
+{
   "packageRules": [
     {
-      "matchPackagePatterns": [ "eslint" ],
+      "matchPackagePatterns": ["eslint"],
       "groupName": "eslint",
       "schedule": ["before 2am on monday"]
     }
   ]
+}
 ```
 
 If you're wondering what is supported and not, under the hood, the schedule is parsed using [@breejs/later](https://github.com/breejs/later) using the `later.parse.text(scheduleString)` API.
@@ -149,15 +155,17 @@ Remember our running `eslint` example?
 Let's automerge it if all the linting updates pass:
 
 ```json
+{
   "packageRules": [
     {
-      "matchPackagePatterns": [ "eslint" ],
+      "matchPackagePatterns": ["eslint"],
       "groupName": "eslint",
       "schedule": ["before 2am on monday"],
       "automerge": true,
       "automergeType": "branch"
     }
   ]
+}
 ```
 
 Have you come up with a rule that you think others would benefit from?
diff --git a/docs/usage/nuget.md b/docs/usage/nuget.md
index 6d5d7af43605cc5f66d5171ee5ca845a29e99ba9..a279985e8951549b8e141b36f76d4d6e9db25884 100644
--- a/docs/usage/nuget.md
+++ b/docs/usage/nuget.md
@@ -30,12 +30,14 @@ Renovate by default performs all lookups on `https://api.nuget.org/v3/index.json
 Alternative feeds can be specified either [in a `NuGet.config` file](https://docs.microsoft.com/en-us/nuget/reference/nuget-config-file#package-source-sections) within your repository (Renovate will not search outside the repository) or in Renovate configuration options:
 
 ```json
-"nuget": {
-  "registryUrls": [
-    "https://api.nuget.org/v3/index.json",
-    "https://example1.com/nuget/",
-    "https://example2.com/nuget/v3/index.json"
-  ]
+{
+  "nuget": {
+    "registryUrls": [
+      "https://api.nuget.org/v3/index.json",
+      "https://example1.com/nuget/",
+      "https://example2.com/nuget/v3/index.json"
+    ]
+  }
 }
 ```
 
@@ -50,10 +52,10 @@ Renovate as a NuGet client supports both versions and will use `v2` unless the c
 If you have a `v3` feed that does not match this pattern (e.g. JFrog Artifactory) you need to help Renovate by appending `#protocolVersion=3` to the registry URL:
 
 ```json
-"nuget": {
-  "registryUrls": [
-    "http://myV3feed#protocolVersion=3"
-  ]
+{
+  "nuget": {
+    "registryUrls": ["http://myV3feed#protocolVersion=3"]
+  }
 }
 ```
 
@@ -62,14 +64,16 @@ If you have a `v3` feed that does not match this pattern (e.g. JFrog Artifactory
 Credentials for authenticated/private feeds can be provided via host rules in the configuration options (file or command line parameter).
 
 ```json
-"hostRules": [
-  {
-    "hostType": "nuget",
-    "matchHost": "http://example1.com/nuget",
-    "username": "root",
-    "password": "p4$$w0rd"
-  }
-]
+{
+  "hostRules": [
+    {
+      "hostType": "nuget",
+      "matchHost": "http://example1.com/nuget",
+      "username": "root",
+      "password": "p4$$w0rd"
+    }
+  ]
+}
 ```
 
 Please note that at the moment only Basic HTTP authentication (via username and password) is supported.
diff --git a/docs/usage/python.md b/docs/usage/python.md
index b753e4e990b02bcaf93dc2be1e07552b12a8178f..96cf715ed6a18b20f17510957c87588997a41ab7 100644
--- a/docs/usage/python.md
+++ b/docs/usage/python.md
@@ -32,9 +32,11 @@ If you have a specific file or file pattern you want the Renovate bot to find, u
 e.g.:
 
 ```json
+{
   "pip_requirements": {
-    "fileMatch": ["my/specifically-named.file", "\.requirements$"]
+    "fileMatch": ["my/specifically-named.file", "\\.requirements$"]
   }
+}
 ```
 
 ## Alternate registries
@@ -63,9 +65,11 @@ You can use the `registryUrls` array to configure alternate index URL(s).
 e.g.:
 
 ```json
+{
   "python": {
     "registryUrls": ["http://example.com/private-pypi/"]
   }
+}
 ```
 
 Note: the index-url found in the `requirements.txt` file takes precedence over a `registryUrl` configured like the above.
@@ -76,14 +80,18 @@ To override the URL found in `requirements.txt`, you need to configure it in `pa
 The most direct way to disable all Python support in Renovate is like this:
 
 ```json
+{
   "python": {
     "enabled": false
   }
+}
 ```
 
 Alternatively, maybe you only want one package manager, such as `npm`.
 In that case this would enable _only_ `npm`:
 
 ```json
+{
   "enabledManagers": ["npm"]
+}
 ```
diff --git a/lib/manager/terragrunt/readme.md b/lib/manager/terragrunt/readme.md
index 00a0d71a653b2c386539b20f9c72aad00ac07958..5c1c9d63a22a6cb9b8fa5599a5ffabb5585b45fa 100644
--- a/lib/manager/terragrunt/readme.md
+++ b/lib/manager/terragrunt/readme.md
@@ -4,8 +4,10 @@ You can create a custom [versioning config](/configuration-options/#versioning)
 For example, if you want to reference a tag like `module-v1.2.5`, a block like this would work:
 
 ```json
-"terraform": {
- "versioning": "regex:^((?<compatibility>.*)-v|v*)(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)$"
+{
+  "terraform": {
+    "versioning": "regex:^((?<compatibility>.*)-v|v*)(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)$"
+  }
 }
 ```
 
diff --git a/package.json b/package.json
index 7ee3abf3f0be3fdb8669f022a5b30c6cfd052c64..48fb1bdebf6c225273e97da57e85c588403f013a 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
     "create-json-schema": "node -r ts-node/register/transpile-only -- bin/create-json-schema.js && prettier --write \"renovate-schema.json\"",
     "debug": "node --inspect-brk -r ts-node/register/transpile-only  -- lib/renovate.ts",
     "doc-fix": "run-s markdown-lint-fix prettier-fix",
+    "doc-fence-check": "node tools/check-fenced-code.mjs",
     "eslint": "eslint --ext .js,.mjs,.ts lib/ test/ tools/ --report-unused-disable-directives",
     "eslint-fix": "eslint --ext .js,.mjs,.ts --fix lib/ test/ tools/ --report-unused-disable-directives",
     "generate": "run-s generate:*",
@@ -23,7 +24,7 @@
     "jest": "cross-env NODE_ENV=test LOG_LEVEL=fatal TZ=UTC node --expose-gc node_modules/jest/bin/jest.js --logHeapUsage",
     "jest-debug": "cross-env NODE_OPTIONS=--inspect-brk yarn jest --testTimeout=100000000",
     "jest-silent": "cross-env yarn jest --reporters jest-silent-reporter",
-    "lint": "run-s ls-lint eslint prettier markdown-lint git-check",
+    "lint": "run-s ls-lint eslint prettier markdown-lint git-check doc-fence-check",
     "lint-fix": "run-s eslint-fix prettier-fix markdown-lint-fix",
     "ls-lint": "ls-lint",
     "markdown-lint": "markdownlint-cli2",
diff --git a/tools/check-fenced-code.mjs b/tools/check-fenced-code.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..32fd71229712efceaaba0abe827a499d618c4964
--- /dev/null
+++ b/tools/check-fenced-code.mjs
@@ -0,0 +1,65 @@
+import { promisify } from 'util';
+import fs from 'fs-extra';
+import g from 'glob';
+import MarkdownIt from 'markdown-it';
+import shell from 'shelljs';
+
+const glob = promisify(g);
+
+const errorTitle = 'Invalid JSON in fenced code block';
+const errorBody =
+  'Fix this manually by ensuring each block is a valid, complete JSON document.';
+const markdownGlob = '{docs,lib}/**/*.md';
+const markdown = new MarkdownIt('zero');
+
+let issues = 0;
+
+markdown.enable(['fence']);
+
+function checkValidJson(file, token) {
+  const start = parseInt(token.map[0], 10) + 1;
+  const end = parseInt(token.map[1], 10) + 1;
+
+  try {
+    JSON.parse(token.content);
+  } catch (err) {
+    issues += 1;
+    if (process.env.CI) {
+      shell.echo(
+        `::error file=${file},line=${start},endLine=${end},title=${errorTitle}::${err.message}. ${errorBody}`
+      );
+    } else {
+      shell.echo(
+        `${errorTitle} (${file} lines ${start}-${end}): ${err.message}`
+      );
+    }
+  }
+}
+
+async function processFile(file) {
+  const text = await fs.readFile(file, 'utf8');
+  const tokens = markdown.parse(text, undefined);
+  shell.echo(`Linting ${file}...`);
+
+  tokens.forEach((token) => {
+    if (token.type === 'fence' && token.info === 'json') {
+      checkValidJson(file, token);
+    }
+  });
+}
+
+// eslint-disable-next-line @typescript-eslint/no-floating-promises
+(async () => {
+  const files = await glob(markdownGlob);
+
+  for (const file of files) {
+    await processFile(file);
+  }
+
+  if (issues) {
+    shell.echo(
+      `${issues} issues found. ${errorBody} See above for lines affected.`
+    );
+    shell.exit(1);
+  }
+})();