diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 4dfc27117c000c1acc9bf5edde9f98e606fe38b9..a70611413ae74d79c2a126eea8ded6c9be5a6b3e 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -8,9 +8,10 @@ import ModelManager from './common/model_manager';
 import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options';
 import { themes } from './themes';
 import languages from './languages';
+import schemas from './schemas';
 import keymap from './keymap.json';
 import { clearDomElement } from '~/editor/utils';
-import { registerLanguages } from '../utils';
+import { registerLanguages, registerSchemas } from '../utils';
 
 function setupThemes() {
   themes.forEach(theme => {
@@ -44,6 +45,7 @@ export default class Editor {
 
     setupThemes();
     registerLanguages(...languages);
+    registerSchemas(...schemas);
 
     this.debouncedUpdate = debounce(() => {
       this.updateDimensions();
diff --git a/app/assets/javascripts/ide/lib/schemas/index.js b/app/assets/javascripts/ide/lib/schemas/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..38a2f81921b5e1df2ccd24cb3ebab4d06e113d0a
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/schemas/index.js
@@ -0,0 +1,4 @@
+import json from './json';
+import yaml from './yaml';
+
+export default [json, yaml];
diff --git a/app/assets/javascripts/ide/lib/schemas/json/index.js b/app/assets/javascripts/ide/lib/schemas/json/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..900d5442bec3fc69e97873b9c46086e869826041
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/schemas/json/index.js
@@ -0,0 +1,8 @@
+export default {
+  language: 'json',
+  options: {
+    validate: true,
+    enableSchemaRequest: true,
+    schemas: [],
+  },
+};
diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js b/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js
new file mode 100644
index 0000000000000000000000000000000000000000..af20744abb381d98d25dcd91adc377316a90365f
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/schemas/yaml/gitlab_ci.js
@@ -0,0 +1,4 @@
+export default {
+  uri: 'https://json.schemastore.org/gitlab-ci',
+  fileMatch: ['*.gitlab-ci.yml'],
+};
diff --git a/app/assets/javascripts/ide/lib/schemas/yaml/index.js b/app/assets/javascripts/ide/lib/schemas/yaml/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..e3fc406df4bca1b62620640434d741d76f2273d2
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/schemas/yaml/index.js
@@ -0,0 +1,12 @@
+import gitlabCi from './gitlab_ci';
+
+export default {
+  language: 'yaml',
+  options: {
+    validate: true,
+    enableSchemaRequest: true,
+    hover: true,
+    completion: true,
+    schemas: [gitlabCi],
+  },
+};
diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js
index c28a2bd9f1d52c2205e9762a7a59e65a60fd4885..9ec7b2c06ce40a0bd3bdc635f5da3f641bd50115 100644
--- a/app/assets/javascripts/ide/utils.js
+++ b/app/assets/javascripts/ide/utils.js
@@ -66,7 +66,7 @@ export const trimPathComponents = path =>
     .join('/');
 
 export function registerLanguages(def, ...defs) {
-  if (defs.length) defs.forEach(lang => registerLanguages(lang));
+  defs.forEach(lang => registerLanguages(lang));
 
   const languageId = def.id;
 
@@ -75,6 +75,19 @@ export function registerLanguages(def, ...defs) {
   languages.setLanguageConfiguration(languageId, def.conf);
 }
 
+export function registerSchemas({ language, options }, ...schemas) {
+  schemas.forEach(schema => registerSchemas(schema));
+
+  const defaults = {
+    json: languages.json.jsonDefaults,
+    yaml: languages.yaml.yamlDefaults,
+  };
+
+  if (defaults[language]) {
+    defaults[language].setDiagnosticsOptions(options);
+  }
+}
+
 export const otherSide = side => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT);
 
 export function trimTrailingWhitespace(content) {
diff --git a/changelogs/unreleased/218472-gitlab-ci-linting.yml b/changelogs/unreleased/218472-gitlab-ci-linting.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0fc937a0a5d9a6d7dc2a319013e41527e6e12185
--- /dev/null
+++ b/changelogs/unreleased/218472-gitlab-ci-linting.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for linting based on schemas in WebIDE
+merge_request: 35838
+author:
+type: added
diff --git a/config/plugins/monaco_webpack.js b/config/plugins/monaco_webpack.js
new file mode 100644
index 0000000000000000000000000000000000000000..7d2837824536591bca5105cbe7762482ab883ace
--- /dev/null
+++ b/config/plugins/monaco_webpack.js
@@ -0,0 +1,17 @@
+const { languagesArr } = require('monaco-editor-webpack-plugin/out/languages');
+
+// monaco-yaml library doesn't play so well with monaco-editor-webpack-plugin
+// so the only way to include its workers is by patching the list of languages
+// in monaco-editor-webpack-plugin and adding support for yaml workers. This is
+// a known issue in the library and this workaround was suggested here:
+// https://github.com/pengx17/monaco-yaml/issues/20
+
+const yamlLang = languagesArr.find(t => t.label === 'yaml');
+
+yamlLang.entry = [yamlLang.entry, '../../monaco-yaml/esm/monaco.contribution'];
+yamlLang.worker = {
+  id: 'vs/language/yaml/yamlWorker',
+  entry: '../../monaco-yaml/esm/yaml.worker.js',
+};
+
+module.exports = require('monaco-editor-webpack-plugin');
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 557db58b1b990d46a5081c363a3657fc92a30b42..8e51ce537c56b3bd508cb519d2a3ce5e011f0155 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -5,7 +5,7 @@ const webpack = require('webpack');
 const VueLoaderPlugin = require('vue-loader/lib/plugin');
 const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
 const CompressionPlugin = require('compression-webpack-plugin');
-const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
+const MonacoWebpackPlugin = require('./plugins/monaco_webpack');
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 const CopyWebpackPlugin = require('copy-webpack-plugin');
 const vendorDllHash = require('./helpers/vendor_dll_hash');
@@ -241,7 +241,7 @@ module.exports = {
       },
       {
         test: /\.(eot|ttf|woff|woff2)$/,
-        include: /node_modules\/katex\/dist\/fonts/,
+        include: /node_modules\/(katex\/dist\/fonts|monaco-editor)/,
         loader: 'file-loader',
         options: {
           name: '[name].[contenthash:8].[ext]',
diff --git a/jest.config.base.js b/jest.config.base.js
index 1a1fd4e7b620b4289a0541bef279a197872147ec..422b6779af42376488783208c2af5def1f3a552d 100644
--- a/jest.config.base.js
+++ b/jest.config.base.js
@@ -82,7 +82,9 @@ module.exports = path => {
       '^.+\\.js$': 'babel-jest',
       '^.+\\.vue$': 'vue-jest',
     },
-    transformIgnorePatterns: ['node_modules/(?!(@gitlab/ui|bootstrap-vue|three|monaco-editor)/)'],
+    transformIgnorePatterns: [
+      'node_modules/(?!(@gitlab/ui|bootstrap-vue|three|monaco-editor|monaco-yaml)/)',
+    ],
     timers: 'fake',
     testEnvironment: '<rootDir>/spec/frontend/environment.js',
     testEnvironmentOptions: {
diff --git a/package.json b/package.json
index 680c850bd9d37d9058dcf83aa4f30711201f5b5b..beeb5cba91da8af6e273540c9395225bd9fa2d8b 100644
--- a/package.json
+++ b/package.json
@@ -97,6 +97,7 @@
     "jquery.caret": "^0.3.1",
     "jquery.waitforimages": "^2.2.0",
     "js-cookie": "^2.2.1",
+    "js-yaml": "^3.13.1",
     "jszip": "^3.1.3",
     "jszip-utils": "^0.0.2",
     "katex": "^0.10.0",
@@ -105,8 +106,9 @@
     "mermaid": "^8.5.2",
     "mersenne-twister": "1.1.0",
     "minimatch": "^3.0.4",
-    "monaco-editor": "^0.18.1",
-    "monaco-editor-webpack-plugin": "^1.7.0",
+    "monaco-editor": "^0.20.0",
+    "monaco-editor-webpack-plugin": "^1.9.0",
+    "monaco-yaml": "^2.4.0",
     "mousetrap": "^1.4.6",
     "pdfjs-dist": "^2.0.943",
     "pikaday": "^1.8.0",
@@ -220,7 +222,7 @@
   },
   "resolutions": {
     "chokidar": "^3.4.0",
-    "monaco-editor": "0.18.1",
+    "monaco-editor": "0.20.0",
     "vue-jest/ts-jest": "24.0.0"
   },
   "engines": {
diff --git a/spec/frontend/__mocks__/monaco-editor/index.js b/spec/frontend/__mocks__/monaco-editor/index.js
index 7c53cfb51740da0b1abd18a76acedaa01492df43..b9602d69b7450f9c7034b24b581ebbbbc88f7a13 100644
--- a/spec/frontend/__mocks__/monaco-editor/index.js
+++ b/spec/frontend/__mocks__/monaco-editor/index.js
@@ -8,9 +8,11 @@ import 'monaco-editor/esm/vs/language/css/monaco.contribution';
 import 'monaco-editor/esm/vs/language/json/monaco.contribution';
 import 'monaco-editor/esm/vs/language/html/monaco.contribution';
 import 'monaco-editor/esm/vs/basic-languages/monaco.contribution';
+import 'monaco-yaml/esm/monaco.contribution';
 
 // This language starts trying to spin up web workers which obviously breaks in Jest environment
 jest.mock('monaco-editor/esm/vs/language/typescript/tsMode');
+jest.mock('monaco-yaml/esm/yamlMode');
 
 export * from 'monaco-editor/esm/vs/editor/editor.api';
 export default global.monaco;
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index a77edfc3fa8944d0bfad61f25f4be7dfe251dbf6..a4336b8f2ebfb29d98e53f13556aedd47b918472 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -637,6 +637,8 @@ describe('RepoEditor', () => {
           // set cursor to line 2, column 1
           vm.editor.instance.setSelection(new Range(2, 1, 2, 1));
           vm.editor.instance.focus();
+
+          jest.spyOn(vm.editor.instance, 'hasTextFocus').mockReturnValue(true);
         });
       });
 
diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js
index f5815771cdf516145749b54eed5e2140f4690f84..0952facf3ee9631163e7beff08dcd82ee77cd903 100644
--- a/spec/frontend/ide/lib/editor_spec.js
+++ b/spec/frontend/ide/lib/editor_spec.js
@@ -199,6 +199,14 @@ describe('Multi-file editor library', () => {
     });
   });
 
+  describe('schemas', () => {
+    it('registers custom schemas defined with Monaco', () => {
+      expect(monacoLanguages.yaml.yamlDefaults.diagnosticsOptions).toMatchObject({
+        schemas: [{ fileMatch: ['*.gitlab-ci.yml'] }],
+      });
+    });
+  });
+
   describe('replaceSelectedText', () => {
     let model;
     let editor;
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index 15baeca7f36124808b93ddaacf85b84e198030ca..b6de576a0a4d31054e182939e2ba458719ba38d0 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -1,6 +1,7 @@
 import {
   isTextFile,
   registerLanguages,
+  registerSchemas,
   trimPathComponents,
   insertFinalNewline,
   trimTrailingWhitespace,
@@ -158,6 +159,57 @@ describe('WebIDE utils', () => {
     });
   });
 
+  describe('registerSchemas', () => {
+    let options;
+
+    beforeEach(() => {
+      options = {
+        validate: true,
+        enableSchemaRequest: true,
+        hover: true,
+        completion: true,
+        schemas: [
+          {
+            uri: 'http://myserver/foo-schema.json',
+            fileMatch: ['*'],
+            schema: {
+              id: 'http://myserver/foo-schema.json',
+              type: 'object',
+              properties: {
+                p1: { enum: ['v1', 'v2'] },
+                p2: { $ref: 'http://myserver/bar-schema.json' },
+              },
+            },
+          },
+          {
+            uri: 'http://myserver/bar-schema.json',
+            schema: {
+              id: 'http://myserver/bar-schema.json',
+              type: 'object',
+              properties: { q1: { enum: ['x1', 'x2'] } },
+            },
+          },
+        ],
+      };
+
+      jest.spyOn(languages.json.jsonDefaults, 'setDiagnosticsOptions');
+      jest.spyOn(languages.yaml.yamlDefaults, 'setDiagnosticsOptions');
+    });
+
+    it.each`
+      language  | defaultsObj
+      ${'json'} | ${languages.json.jsonDefaults}
+      ${'yaml'} | ${languages.yaml.yamlDefaults}
+    `(
+      'registers the given schemas with monaco for lang: $language',
+      ({ language, defaultsObj }) => {
+        registerSchemas({ language, options });
+
+        expect(defaultsObj.setDiagnosticsOptions).toHaveBeenCalledWith(options);
+      },
+    );
+  });
+
   describe('trimTrailingWhitespace', () => {
     it.each`
       input                                                            | output
diff --git a/yarn.lock b/yarn.lock
index a4de32ca84a2157f3ce6248664d2b4856ae71970..62770e0fa64327b824903db6a611f21f8b88d412 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1171,11 +1171,6 @@
   dependencies:
     "@toast-ui/editor" "^2.2.0"
 
-"@types/anymatch@*":
-  version "1.3.0"
-  resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.0.tgz#d1d55958d1fccc5527d4aba29fc9c4b942f563ff"
-  integrity sha512-7WcbyctkE8GTzogDb0ulRAEw7v8oIS54ft9mQTU7PfM0hp5e+8kpa+HeQ7IQrFbKtJXBKcZ4bh+Em9dTw5L6AQ==
-
 "@types/babel__core@^7.1.0":
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.2.tgz#608c74f55928033fce18b99b213c16be4b3d114f"
@@ -1285,11 +1280,6 @@
   resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
   integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
 
-"@types/tapable@*":
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
-  integrity sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==
-
 "@types/tern@*":
   version "0.23.3"
   resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"
@@ -1297,13 +1287,6 @@
   dependencies:
     "@types/estree" "*"
 
-"@types/uglify-js@*":
-  version "3.0.4"
-  resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
-  integrity sha512-SudIN9TRJ+v8g5pTG8RRCqfqTMNqgWCKKd3vtynhGzkIIjxaicNAMuY5TRadJ6tzDu3Dotf3ngaMILtmOdmWEQ==
-  dependencies:
-    source-map "^0.6.1"
-
 "@types/unist@*", "@types/unist@^2.0.0":
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e"
@@ -1326,17 +1309,6 @@
     "@types/unist" "*"
     "@types/vfile-message" "*"
 
-"@types/webpack@^4.4.19":
-  version "4.4.23"
-  resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.23.tgz#059d6f4598cfd65ddee0e2db38317ef989696712"
-  integrity sha512-WswyG+2mRg0ul/ytPpCSWo+kOlVVPW/fKCBEVwqmPVC/2ffWEwhsCEQgnFbWDf8EWId2qGcpL623EjLfNTRk9A==
-  dependencies:
-    "@types/anymatch" "*"
-    "@types/node" "*"
-    "@types/tapable" "*"
-    "@types/uglify-js" "*"
-    source-map "^0.6.0"
-
 "@types/yargs-parser@*":
   version "15.0.0"
   resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d"
@@ -8252,17 +8224,24 @@ moment-mini@^2.22.1:
   resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.22.1.tgz#bc32d73e43a4505070be6b53494b17623183420d"
   integrity sha512-OUCkHOz7ehtNMYuZjNciXUfwTuz8vmF1MTbAy59ebf+ZBYZO5/tZKuChVWCX+uDo+4idJBpGltNfV8st+HwsGw==
 
-monaco-editor-webpack-plugin@^1.7.0:
-  version "1.7.0"
-  resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz#920cbeecca25f15d70d568a7e11b0ba4daf1ae83"
-  integrity sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA==
+monaco-editor-webpack-plugin@^1.9.0:
+  version "1.9.0"
+  resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.9.0.tgz#5b547281b9f404057dc5d8c5722390df9ac90be6"
+  integrity sha512-tOiiToc94E1sb50BgZ8q8WK/bxus77SRrwCqIpAB5er3cpX78SULbEBY4YPOB8kDolOzKRt30WIHG/D6gz69Ww==
   dependencies:
-    "@types/webpack" "^4.4.19"
+    loader-utils "^1.2.3"
 
-monaco-editor@0.18.1, monaco-editor@^0.18.1:
-  version "0.18.1"
-  resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.18.1.tgz#ced7c305a23109875feeaf395a504b91f6358cfc"
-  integrity sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw==
+monaco-editor@0.20.0, monaco-editor@^0.20.0:
+  version "0.20.0"
+  resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"
+  integrity sha512-hkvf4EtPJRMQlPC3UbMoRs0vTAFAYdzFQ+gpMb8A+9znae1c43q8Mab9iVsgTcg/4PNiLGGn3SlDIa8uvK1FIQ==
+
+monaco-yaml@^2.4.0:
+  version "2.4.0"
+  resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-2.4.0.tgz#027307a231d809c416babf1cf89b4c1bb940e55d"
+  integrity sha512-ElUS6uBqEjA2/o2gLuNdnqWSAAQXh8ISr1kwlFErm3t5IXO74TNfS3gnjO6Kv9TXS7LImjGfgPAZei7o8zNTHw==
+  optionalDependencies:
+    prettier "^1.19.1"
 
 mousetrap@^1.4.6:
   version "1.4.6"
@@ -9387,11 +9366,16 @@ prettier@1.16.3:
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d"
   integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==
 
-prettier@1.18.2, prettier@^1.18.2:
+prettier@1.18.2:
   version "1.18.2"
   resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
   integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
 
+prettier@^1.18.2, prettier@^1.19.1:
+  version "1.19.1"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
+  integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
+
 pretty-format@^24.8.0:
   version "24.8.0"
   resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.8.0.tgz#8dae7044f58db7cb8be245383b565a963e3c27f2"