Fixing the YAML Language Server

2025-09-14
author
Rico Berger
Site Reliability Engineer

First, I want to apologize for the clickbait title. The YAML Language Server is a great project, and I don't want to disrespect the maintainers. However, while reworking my Neovim configuration, I encountered some issues with the YAML Language Server that I would like to share with you, along with how I resolved them.

YAML Language Server

Motivation

While reworking my Neovim configuration, I also decided to adjust the YAML Language Server settings to support completion and validation for Kubernetes manifests. I began with the following configuration:

yaml = {
  format = {
    enable = false,
  },
  completion = true,
  hover = true,
  validate = true,
  schemas = {
    kubernetes = {
      "/kubernetes/**/*.yml",
      "/kubernetes/**/*.yaml",
    },
  },
  schemaStore = {
    enable = false,
    url = "",
  },
},

This worked well for all standard Kubernetes manifests. The problems began when I started working with CustomResourceDefinitions (CRDs), as I consistently encountered the following error:

kubernetes/namespaces/default/example-vs.yaml|2 col 7-21 error   1| Value is not accepted. Valid values: "ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding", "MutatingAdmissionPolicy", "MutatingAdmissionPolicyBinding", "StorageVersion", "DaemonSet", "Deployment", "ReplicaSet", "StatefulSet", "TokenRequest", "TokenReview", "LocalSubjectAccessReview", "SelfSubjectAccessReview", "SelfSubjectRulesReview", "SubjectAccessReview", "HorizontalPodAutoscaler", "Scale", "CronJob", "Job", "CertificateSigningRequest", "ClusterTrustBundle", "Lease", "LeaseCandidate", "LimitRange", "Namespace", "Node", "PersistentVolume", "PersistentVolumeClaim", "Pod", "ReplicationController", "ResourceQuota", "Service", "FlowSchema", "PriorityLevelConfiguration", "Ingress", "IngressClass", "NetworkPolicy", "IPAddress", "ServiceCIDR", "PodDisruptionBudget", "DeviceClass", "ResourceClaim", "ResourceClaimTemplate", "ResourceSlice", "CSIDriver", "CSINode", "VolumeAttachment", "StorageVersionMigration", "CustomResourceDefinition", "APIService".

This was somewhat expected, as the YAML Language Server is unaware of the CustomResourceDefinitions I am using. To resolve this, there are two options:

  1. Add the schema for each CustomResourceDefinition to the YAML Language Server configuration. The issue with this approach is that I would need to define a unique pattern for each file, which is not feasible.
schemas = {
  kubernetes = {
    "*-deploy.yml",
    "*-deploy.yaml",
    ...
  },
  ["https://raw.githubusercontent.com/datreeio/CRDs-catalog/refs/heads/main/networking.istio.io/virtualservice_v1.json"] = {
    "*-vs.yml",
    "*-vs.yaml",
  }
}
  1. Add an annotation to all CustomResourceDefinitions, which was also not a feasible solution for me.
# yaml-language-server: $schema=https://raw.githubusercontent.com/datreeio/CRDs-catalog/refs/heads/main/networking.istio.io/virtualservice_v1.json
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: example
  namespace: default
spec: ...

After conducting some research, I decided to create my own JSON schema for Kubernetes, which includes the Kubernetes JSON schema and the schemas for all CustomResourceDefinitions I am using. The configuration for the YAML Language Server to utilize the new schema is as follows:

schemas = {
  ["https://raw.githubusercontent.com/ricoberger/kubernetes-json-schema/refs/heads/main/schemas/all.json"] = {
    "/kubernetes/**/*.yml",
    "/kubernetes/**/*.yaml",
  }
}

The problem with this approach is that the YAML Language Server has extensive logic related to the default Kubernetes schema, which prevents it from functioning as expected. With the configuration mentioned above, I consistently received the following error:

kubernetes/namespaces/default/example-deploy.yaml|2 col 1-2 error| Matches multiple schemas when only one must validate.

This is a known issue with the YAML Language Server. At that point, I was so frustrated that I decided to fork the YAML Language Server to add an option to overwrite the default Kubernetes schema.

With this small change, everything is now functioning as expected. I receive completion and validation for all Kubernetes manifests, including the CustomResourceDefinitions I am using.

Usage

In the following section, we will explore how to use the forked version of the YAML Language Server and the custom Kubernetes JSON schema with Neovim.

First, we need to clone the repository, check out the branch with the modified version, and build the YAML Language Server.

git clone git@github.com:ricoberger/yaml-language-server.git
cd yaml-language-server
git checkout add-option-to-overwrite-kubernetes-schema

npm install
npm run build

In the next step, we will create a bash script yamlls in our PATH to set the YAMLLS_KUBERNETES_SCHEMA_URL environment variable and to start our custom YAML Language Server.

#!/usr/bin/env bash

export YAMLLS_KUBERNETES_SCHEMA_URL="https://raw.githubusercontent.com/ricoberger/kubernetes-json-schema/refs/heads/main/schemas/all.json"

node /Users/ricoberger/Documents/GitHub/ricoberger/yaml-language-server/out/server/src/server.js $@

Lastly, we configure the YAML Language Server in Neovim (yamlls.lua) to use our yamlls script for starting the server.

return {
  cmd = { "yamlls", "--stdio" },
  filetypes = {
    "yaml",
    "yaml.docker-compose",
    "yaml.gitlab",
    "yaml.helm-values",
  },
  single_file_support = true,
  settings = {
    redhat = {
      telemetry = {
        enabled = false,
      },
    },
    yaml = {
      format = {
        enable = false,
      },
      completion = true,
      hover = true,
      validate = true,
      schemas = {
        kubernetes = {
          "/kubernetes/**/*.yml",
          "/kubernetes/**/*.yaml",
        },
      },
      schemaStore = {
        enable = false,
        url = "",
      },
    },
  },
  on_init = function(client)
    client.server_capabilities.documentFormattingProvider = nil
    client.server_capabilities.documentRangeFormattingProvider = nil
  end,
}

If you are using the Helm Language Server, you can adjust the configuration in helm_ls.lua to also utilize the yamlls script for starting the YAML Language Server.

Schema Generation

In the last section of this blog post, we will examine the ricoberger/kubernetes-json-schema repository and explain how the generation of the JSON schema works.

The JSON schema is generated by the generate.sh script. First, we create a new kind cluster and apply all CustomResourceDefinitions (CRDs) from the crds directory. Next, we start a kubectl proxy to access the Kubernetes API of the kind cluster.

kind create cluster --image=kindest/node:v1.34.0
kubectl apply --server-side -f crds/
kubectl proxy --port=5555 --accept-hosts='^.*'

The openapi2jsonschema.py script is used to fetch the OpenAPI v2 definition from the kind cluster and convert it to JSON schema. The generated JSON schema files are stored in the schemas directory.

openapi2jsonschema.py "schemas" "http://127.0.0.1:5555/openapi/v2"

Wrapping Up

I'm still unsure whether I'm missing something obvious or if the YAML Language Server is simply not designed to handle this use case. The latter suggests that there are several issues related to the handling of CustomResourceDefinitions (824, #841, #962, and #1050). If you have any suggestions or improvements, please let me know. I would be happy to hear your thoughts.

# neovim
# kubernetes