CUE has first class support for JSON Schema: both the cue command and the Go API can convert between CUE and JSON Schema in both directions.

Constraints stored as JSON Schema are available for cue commands to use as if they were expressed in CUE. This allows you to work with JSON Schema constraints directly, using them to validate data, and to represent them natively in CUE’s more succinct and expressive form. CUE definitions can also be exported as JSON Schema, letting you share your CUE schemas with tools that understand JSON Schema but not CUE.

In this guide we’ll see:

Using JSON Schema with the cue command

The cue import command can produce CUE from JSON Schema.

Let’s start with this JSON Schema:

schema.json
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "title": "Main Person schema.",
    "description": "This schema defines a person.",
    "required": [
        "name"
    ],
    "properties": {
        "name": {
            "description": "What is this person called?",
            "type": "string",
            "minLength": 1
        },
        "address": {
            "description": "Where does this person live?",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
        },
        "children": {
            "description": "This is a very long comment for some reason, which will keep going and going past the point where it should probably have stopped.",
            "type": "array",
            "items": {
                "type": "string"
            },
            "default": null
        },
        "home phone": {
            "type": "string",
            "deprecated": true
        }
    }
}

We use cue import to convert the JSON Schema to CUE:

TERMINAL
$ cue import -l '#Person:' schema.json

cue import recognises JSON Schema from its signature fields, and uses the schema’s constraints to create a shorter, more readable CUE representation. Our -l parameter tells cue to place the constraints inside the #Person definition:

schema.cue
// Main Person schema.
//
// This schema defines a person.

import "strings"

#Person: {
	@jsonschema(schema="https://json-schema.org/draft/2020-12/schema")

	// What is this person called?
	name!: strings.MinRunes(1)

	// Where does this person live?
	address?: strings.MinRunes(1) & strings.MaxRunes(200)

	// This is a very long comment for some reason, which will keep
	// going and going past the point where it should probably have
	// stopped.
	children?: [...string]
	"home phone"?: string @deprecated()
	...
}

We use the imported schema to validate some known-good data (good.json) and known-bad data (bad.json):

good.json
{
    "name": "Dorothy Cartwright",
    "address": "Ripon, North Yorkshire"
}
bad.json
{
    "name": [
        "Charlie",
        "Cartwright"
    ],
    "address": "Ripon, North Yorkshire"
}

The cue vet command validates our data against the #Person constraint:

TERMINAL
$ cue vet -c -d '#Person' schema.cue good.json bad.json
name: conflicting values ["Charlie","Cartwright"] and strings.MinRunes(1) (mismatched types list and string):
    ./bad.json:2:13
    ./schema.cue:11:9

The cue vet command can also validate the data using the JSON Schema directly:

TERMINAL
$ cue vet -c schema.json good.json bad.json
name: conflicting values ["Charlie","Cartwright"] and strings.MinRunes(1) (mismatched types list and string):
    ./bad.json:2:13
    ./schema.json:13:14

The cue command normally recognises JSON Schema’s signature fields and treats the contents of JSON Schema as data constraints - not just additional data. A qualifier can be used to change this behaviour, as outlined in cue help filetypes:

TERMINAL
$ cue def json: schema.json
$schema:     "https://json-schema.org/draft/2020-12/schema"
type:        "object"
title:       "Main Person schema."
description: "This schema defines a person."
...

Using JSON Schema with the Go API

The encoding/jsonschema API allows you to work with JSON Schema in Go code.

As with the cue command examples shown above, the API can be used to convert JSON Schema to CUE. However, in this next example, we’ll use the API in a more fully-formed context: controlling data validation at a lower level.

This Go program validates a JSON data file against a JSON Schema:

main.go
package main

import (
	"flag"
	"fmt"
	"log"
	"os"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/cuecontext"
	"cuelang.org/go/cue/errors"
	"cuelang.org/go/encoding/json"
	"cuelang.org/go/encoding/jsonschema"
)

func main() {
	log.SetFlags(0)
	flag.Parse()
	args := flag.Args()

	// A cue.Context is used for building/compiling CUE at a low-level.
	// It replaces cue.Runtime.
	ctx := cuecontext.New()

	if len(args) != 2 {
		log.Fatalf("usage:\n\t%s SCHEMA.json DATA.json\n", os.Args[0])
	}

	// Load the schema file JSON
	schemaFile, err := os.ReadFile(args[0])
	if err != nil {
		log.Fatal(err)
	}
	schemaJsonAst, err := json.Extract(args[0], schemaFile)
	if err != nil {
		log.Fatal(err)
	}
	schemaJson := ctx.BuildExpr(schemaJsonAst)

	// Extract JSON Schema from the JSON
	schemaAst, err := jsonschema.Extract(schemaJson, &jsonschema.Config{
		Strict: true,
	})
	if err != nil {
		log.Fatal(err)
	}

	// Build a cue.Value of the schema
	schema := ctx.BuildFile(schemaAst)

	// Load the data file JSON
	dataFile, err := os.ReadFile(args[1])
	if err != nil {
		log.Fatal(err)
	}
	dataAst, err := json.Extract(args[1], dataFile)
	if err != nil {
		log.Fatal(err)
	}

	// Build a cue.Value of the data
	data := ctx.BuildExpr(dataAst)

	// Unify the schema and data
	res := schema.Unify(data)

	// Validate whether the combined (unified) result has errors or not.
	if err := res.Validate(cue.Concrete(true)); err != nil {
		// If errors, report them and fail.
		log.Fatal(errors.Details(err, nil))
	}
	// If no errors, print the data value
	fmt.Printf("%v\n", res)
}
Running the command validates the data file in the second argument against the JSON schema in the first argument - printing the data if it’s valid and displaying a validation error otherwise. Here we use it to validate the same good.json and bad.json files from above:

TERMINAL
$ go run . schema.json good.json
{
	name:    "Dorothy Cartwright"
	address: "Ripon, North Yorkshire"
}
$ go run . schema.json bad.json
name: conflicting values ["Charlie","Cartwright"] and strings.MinRunes(1) (mismatched types list and string):
    bad.json:2:13
    schema.json:13:14
exit status 1

Generating JSON Schema from CUE

The cue def command can produce JSON Schema from CUE definitions.

Let’s start with a CUE definition:

generate_schema.cue
@experiment(explicitopen)

#Team: {
	name: string
	members: [...string]
	lead?: string
}

The @experiment(explicitopen) attribute makes the open and closed status of CUE structs translate predictably into JSON Schema, so we recommend enabling it whenever you generate JSON Schema. See cue help experiments for details about the experiment itself.

We use cue def with the --out jsonschema flag to generate a JSON Schema, selecting the #Team definition with -e:

TERMINAL
$ cue def --out jsonschema -e '#Team' generate_schema.cue
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {
        "lead": {
            "type": "string"
        },
        "members": {
            "type": "array",
            "items": {
                "type": "string"
            }
        },
        "name": {
            "type": "string"
        }
    },
    "required": [
        "name"
    ]
}

The generated schema faithfully represents the CUE constraints: required fields become entries in the required array, optional fields are omitted from it, and the list type becomes a JSON Schema array with typed items.

The output uses Draft 2020-12, which is currently the only JSON Schema version that cue generates.

Open and closed structs

CUE definitions are closed by default, meaning they reject fields not mentioned in the definition. Adding ... to a definition makes it open, allowing additional fields. This maps directly to additionalProperties in the generated JSON Schema:

open.cue
@experiment(explicitopen)

#Closed: {
	name: string
}

#Open: {
	name: string
	...
}

The closed definition produces "additionalProperties": false:

TERMINAL
$ cue def --out jsonschema -e '#Closed' open.cue
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {
        "name": {
            "type": "string"
        }
    },
    "required": [
        "name"
    ]
}

The open definition produces "additionalProperties": true, allowing the schema to accept fields beyond those explicitly listed:

TERMINAL
$ cue def --out jsonschema -e '#Open' open.cue
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": true,
    "properties": {
        "name": {
            "type": "string"
        }
    },
    "required": [
        "name"
    ]
}

References between definitions

When one definition references another, the referenced definition appears in the $defs section of the generated schema:

defs.cue
@experiment(explicitopen)

#Address: {
	street: string
	city:   string
	zip:    string
}

#Person: {
	name:    string
	address: #Address
}
TERMINAL
$ cue def --out jsonschema -e '#Person' defs.cue
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$defs": {
        "#Address": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
                "city": {
                    "type": "string"
                },
                "street": {
                    "type": "string"
                },
                "zip": {
                    "type": "string"
                }
            },
            "required": [
                "city",
                "street",
                "zip"
            ]
        }
    },
    "type": "object",
    "additionalProperties": false,
    "properties": {
        "address": {
            "$ref": "#/$defs/%23Address"
        },
        "name": {
            "type": "string"
        }
    },
    "required": [
        "address",
        "name"
    ]
}

Writing output to a file

Use the -o flag to write the generated schema directly to a file:

TERMINAL
$ cue def --out jsonschema -e '#Closed' -o closed.schema.json open.cue
$ cat closed.schema.json
{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {
        "name": {
            "type": "string"
        }
    },
    "required": [
        "name"
    ]
}

Generating JSON Schema with the Go API

The encoding/jsonschema API can also generate JSON Schema from CUE values.

This Go program takes a CUE file and definition name, and prints the corresponding JSON Schema:

gen/main.go
package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"os"

	"cuelang.org/go/cue"
	"cuelang.org/go/cue/cuecontext"
	"cuelang.org/go/encoding/jsonschema"
)

func main() {
	log.SetFlags(0)
	flag.Parse()
	args := flag.Args()

	if len(args) != 2 {
		log.Fatalf("usage:\n\t%s FILE.cue '#Definition'\n", os.Args[0])
	}

	ctx := cuecontext.New()

	src, err := os.ReadFile(args[0])
	if err != nil {
		log.Fatal(err)
	}
	val := ctx.CompileBytes(src)
	def := val.LookupPath(cue.ParsePath(args[1]))

	schema, err := jsonschema.Generate(def, nil)
	if err != nil {
		log.Fatal(err)
	}
	data, err := json.MarshalIndent(schema, "", "    ")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s\n", data)
}
TERMINAL
$ go run ./gen generate_schema.cue '#Team'
{
    "Lbrace": {},
    "Elts": [
        {
            "Label": {
                "NamePos": {},
                "Name": "$schema",
                "Scope": null,
                "Node": null
            },
...