encoding/jsonschema: improve handling of $id and $ref

Fixes most issues in Issue #378. Remaining problems are mostly
related to invalid JSON schema, if one counts the lack of an $id
field as an invalid schema.

Used Draft 8 (2019-09) as reference.

- Keep stack of $ids
- resolve nested $ids and $refs relative to top stack
- Fixes several bugs
- Allow nested definitions
- Allow references to non-definitions

Note:
- OpenAPI still has its own mechanism. This should be improved at
some point to be more unified with JSON schema.

Also:
- copy metadata in Sanitize

Fixes #378. File separate issues for remaining issues.

Change-Id: I310d13fe378ff4837c13336d01e0102cb6d49382
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6142
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
Reviewed-by: Paul Jolly <paul@myitcv.org.uk>
diff --git a/cmd/cue/cmd/testdata/script/def_jsonschema.txt b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
index 551374f..5e0fce7 100644
--- a/cmd/cue/cmd/testdata/script/def_jsonschema.txt
+++ b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
@@ -69,10 +69,10 @@
 
 -- expect-stderr2 --
 age: conflicting values "twenty" and >=0 (mismatched types string and number):
-    13:7
+    14:7
     ./data.yaml:1:7
 -- expect-stderr3 --
 age: conflicting values "twenty" and >=0 (mismatched types string and number):
-    13:7
+    14:7
     ./data.yaml:1:7
 -- cue.mod --
diff --git a/cue/ast/astutil/sanitize.go b/cue/ast/astutil/sanitize.go
index c5b3092..2980be7 100644
--- a/cue/ast/astutil/sanitize.go
+++ b/cue/ast/astutil/sanitize.go
@@ -291,7 +291,13 @@
 			var isNew bool
 			name, isNew = z.addRename(y.Name, x)
 			if isNew {
-				x.Label = &ast.Alias{Ident: ast.NewIdent(name), Expr: y}
+				ident := ast.NewIdent(name)
+				// Move formatting and comments from original label to alias
+				// identifier.
+				CopyMeta(ident, y)
+				ast.SetRelPos(y, token.NoRelPos)
+				ast.SetComments(y, nil)
+				x.Label = &ast.Alias{Ident: ident, Expr: y}
 			}
 
 		default:
diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go
index ffac830..a9260c6 100644
--- a/encoding/jsonschema/constraints.go
+++ b/encoding/jsonschema/constraints.go
@@ -15,10 +15,14 @@
 package jsonschema
 
 import (
+	"fmt"
 	"math/big"
+	"path"
+	"regexp"
 
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
 	"cuelang.org/go/internal"
 )
@@ -81,25 +85,32 @@
 		s.errf(n, `"definitions" expected an object, found %s`, n.Kind())
 	}
 
-	if len(s.path) != 1 {
-		s.errf(n, `"definitions" only allowed at root`)
-	}
-
 	old := s.isSchema
 	s.isSchema = true
 	defer func() { s.isSchema = old }()
 
 	s.processMap(n, func(key string, n cue.Value) {
-		name := s.path[len(s.path)-1]
-		a, _ := jsonSchemaRef(n.Pos(), []string{"definitions", name})
+		name := key
 
-		f := &ast.Field{Label: a[len(a)-1], Value: s.schema(n)}
-		for i := len(a) - 2; i >= 0; i-- {
-			f = &ast.Field{Label: a[i], Value: ast.NewStruct(f)}
+		var f *ast.Field
+
+		ident := "#" + name
+		if ast.IsValidIdent(ident) {
+			f = &ast.Field{Value: s.schema(n, label{ident, true})}
+			f.Label = ast.NewIdent(ident)
+		} else {
+			f = &ast.Field{Value: s.schema(n, label{"#", true}, label{name: name})}
+			f.Label = ast.NewString(name)
+			ident = "#"
+			f = &ast.Field{
+				Label: ast.NewIdent("#"),
+				Value: ast.NewStruct(f),
+			}
 		}
 
 		ast.SetRelPos(f, token.NewSection)
 		s.definitions = append(s.definitions, f)
+		s.setField(label{name: ident, isDef: true}, f)
 	})
 }
 
@@ -114,11 +125,32 @@
 
 	p0("$id", func(n cue.Value, s *state) {
 		// URL: https://domain.com/schemas/foo.json
-		// Use Title(foo) as CUE identifier.
 		// anchors: #identifier
 		//
 		// TODO: mark identifiers.
-		s.id, _ = s.strValue(n)
+
+		// Resolution must be relative to parent $id
+		// https://tools.ietf.org/html/draft-handrews-json-schema-02#section-8.2.2
+		u := s.resolveURI(n)
+		if u == nil {
+			return
+		}
+
+		if u.Fragment != "" {
+			if s.cfg.Strict {
+				s.errf(n, "$id URI may not contain a fragment")
+			}
+			return
+		}
+		s.id = u
+
+		if s.obj == nil {
+			s.obj = &ast.StructLit{}
+		}
+		// TODO: handle the case where this is always defined and we don't want
+		// to include the default value.
+		s.obj.Elts = append(s.obj.Elts, &ast.Attribute{
+			Text: fmt.Sprintf("@jsonschema(id=%q)", u)})
 	}),
 
 	// Generic constraint
@@ -221,30 +253,22 @@
 	p1("definitions", addDefinitions),
 	p1("$ref", func(n cue.Value, s *state) {
 		s.usedTypes = allTypes
-		str, _ := s.strValue(n)
-		refs := s.parseRef(n.Pos(), str)
-		var a []ast.Label
-		if refs != nil {
-			a = s.mapRef(n.Pos(), str, refs)
-		}
-		if a == nil {
-			s.addConjunct(&ast.BadExpr{From: n.Pos()})
+
+		u := s.resolveURI(n)
+
+		if u.Fragment != "" && !path.IsAbs(u.Fragment) {
+			s.addErr(errors.Newf(n.Pos(), "anchors (%s) not supported", u.Fragment))
+			// TODO: support anchors
 			return
 		}
-		sel, ok := a[0].(ast.Expr)
-		if !ok {
-			sel = &ast.BadExpr{}
-		}
-		for _, l := range a[1:] {
-			switch x := l.(type) {
-			case *ast.Ident:
-				sel = &ast.SelectorExpr{X: sel, Sel: x}
 
-			case *ast.BasicLit:
-				sel = &ast.IndexExpr{X: sel, Index: x}
-			}
+		expr := s.makeCUERef(n, u)
+
+		if expr == nil {
+			expr = &ast.BadExpr{From: n.Pos()}
 		}
-		s.addConjunct(sel)
+
+		s.addConjunct(expr)
 	}),
 
 	// Combinators
@@ -270,7 +294,7 @@
 	p2("allOf", func(n cue.Value, s *state) {
 		var a []ast.Expr
 		for _, v := range s.listItems("allOf", n, false) {
-			x, sub := s.schemaState(v, s.allowedTypes, true)
+			x, sub := s.schemaState(v, s.allowedTypes, nil, true)
 			s.allowedTypes &= sub.allowedTypes
 			s.usedTypes |= sub.usedTypes
 			if sub.hasConstraints() {
@@ -286,7 +310,7 @@
 		var types cue.Kind
 		var a []ast.Expr
 		for _, v := range s.listItems("anyOf", n, false) {
-			x, sub := s.schemaState(v, s.allowedTypes, true)
+			x, sub := s.schemaState(v, s.allowedTypes, nil, true)
 			types |= sub.allowedTypes
 			if sub.hasConstraints() {
 				a = append(a, x)
@@ -303,7 +327,7 @@
 		var a []ast.Expr
 		hasSome := false
 		for _, v := range s.listItems("oneOf", n, false) {
-			x, sub := s.schemaState(v, s.allowedTypes, true)
+			x, sub := s.schemaState(v, s.allowedTypes, nil, true)
 			types |= sub.allowedTypes
 
 			// TODO: make more finegrained by making it two pass.
@@ -328,6 +352,13 @@
 	// String constraints
 
 	p1("pattern", func(n cue.Value, s *state) {
+		str, _ := n.String()
+		if _, err := regexp.Compile(str); err != nil {
+			if s.cfg.Strict {
+				s.errf(n, "unsupported regexp: %v", err)
+			}
+			return
+		}
 		s.usedTypes |= cue.StringKind
 		s.addConjunct(&ast.UnaryExpr{Op: token.MAT, X: s.string(n)})
 	}),
@@ -411,9 +442,9 @@
 
 		s.processMap(n, func(key string, n cue.Value) {
 			// property?: value
-			label := ast.NewString(key)
-			expr, state := s.schemaState(n, allTypes, false)
-			f := &ast.Field{Label: label, Value: expr}
+			name := ast.NewString(key)
+			expr, state := s.schemaState(n, allTypes, []label{{name: key}}, false)
+			f := &ast.Field{Label: name, Value: expr}
 			state.doc(f)
 			f.Optional = token.Blank.Pos()
 			if len(s.obj.Elts) > 0 && len(f.Comments()) > 0 {
@@ -424,12 +455,13 @@
 			if state.deprecated {
 				switch expr.(type) {
 				case *ast.StructLit:
-					s.obj.Elts = append(s.obj.Elts, addTag(label, "deprecated", ""))
+					s.obj.Elts = append(s.obj.Elts, addTag(name, "deprecated", ""))
 				default:
 					f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", ""))
 				}
 			}
 			s.obj.Elts = append(s.obj.Elts, f)
+			s.setField(label{name: key}, f)
 		})
 	}),
 
@@ -481,14 +513,14 @@
 
 	p1d("propertyNames", 6, func(n cue.Value, s *state) {
 		// [=~pattern]: _
-		if names, _ := s.schemaState(n, cue.StringKind, false); !isAny(names) {
+		if names, _ := s.schemaState(n, cue.StringKind, nil, false); !isAny(names) {
 			s.usedTypes |= cue.StructKind
 			s.addConjunct(ast.NewStruct(ast.NewList((names)), ast.NewIdent("_")))
 		}
 	}),
 
 	// TODO: reenable when we have proper non-monotonic contraint validation.
-	// p0("minProperties", func(n cue.Value, s *state) {
+	// p1("minProperties", func(n cue.Value, s *state) {
 	// 	s.usedTypes |= cue.StructKind
 
 	// 	pkg := s.addImport("struct")
@@ -589,7 +621,7 @@
 		case cue.ListKind:
 			var a []ast.Expr
 			for _, n := range s.listItems("items", n, true) {
-				v := s.schema(n)
+				v := s.schema(n) // TODO: label with number literal.
 				ast.SetRelPos(v, token.NoRelPos)
 				a = append(a, v)
 			}
diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go
index a4cd51a..cbc7bb1 100644
--- a/encoding/jsonschema/decode.go
+++ b/encoding/jsonschema/decode.go
@@ -21,6 +21,7 @@
 import (
 	"fmt"
 	"math/bits"
+	"net/url"
 	"strings"
 
 	"cuelang.org/go/cue"
@@ -39,11 +40,9 @@
 
 // A decoder converts JSON schema to CUE.
 type decoder struct {
-	cfg *Config
-
-	errs errors.Error
-
-	definitions []ast.Decl
+	cfg   *Config
+	errs  errors.Error
+	numID int // for creating unique numbers: increment on each use
 }
 
 // addImport registers
@@ -93,7 +92,6 @@
 	}
 
 	f.Decls = append(f.Decls, a...)
-	f.Decls = append(f.Decls, d.definitions...)
 
 	_ = astutil.Sanitize(f)
 
@@ -111,15 +109,12 @@
 		root.isSchema = true
 	}
 
-	expr, state := root.schemaState(v, allTypes, false)
+	expr, state := root.schemaState(v, allTypes, nil, false)
 
 	tags := []string{}
 	if state.jsonschema != "" {
 		tags = append(tags, fmt.Sprintf("schema=%q", state.jsonschema))
 	}
-	if state.id != "" {
-		tags = append(tags, fmt.Sprintf("id=%q", state.id))
-	}
 
 	if name == nil {
 		if len(tags) > 0 {
@@ -164,6 +159,16 @@
 		expr = ast.NewStruct(ref[i], expr)
 	}
 
+	if root.hasSelfReference {
+		return []ast.Decl{
+			&ast.EmbedDecl{Expr: ast.NewIdent(topSchema)},
+			&ast.Field{
+				Label: ast.NewIdent(topSchema),
+				Value: &ast.StructLit{Elts: a},
+			},
+		}
+	}
+
 	return a
 }
 
@@ -224,10 +229,14 @@
 
 	isSchema bool // for omitting ellipsis in an ast.File
 
+	up     *state
 	parent *state
 
 	path []string
 
+	// idRef is used to refer to this schema in case it defines an $id.
+	idRef []label
+
 	pos cue.Value
 
 	typeOptional bool
@@ -240,17 +249,35 @@
 	description string
 	deprecated  bool
 	jsonschema  string
-	id          string
+	id          *url.URL // base URI for $ref
+
+	definitions []ast.Decl
 
 	conjuncts []ast.Expr
 
-	obj         *ast.StructLit
+	// Used for inserting definitions, properties, etc.
+	hasSelfReference bool
+	obj              *ast.StructLit
+	// Complete at finalize.
+	fieldRefs map[label]refs
+
 	closeStruct bool
 	patterns    []ast.Expr
 
 	list *ast.ListLit
 }
 
+type label struct {
+	name  string
+	isDef bool
+}
+
+type refs struct {
+	field *ast.Field
+	ident string
+	refs  []*ast.Ident
+}
+
 func (s *state) hasConstraints() bool {
 	return len(s.conjuncts) > 0 ||
 		len(s.patterns) > 0 ||
@@ -332,6 +359,20 @@
 		}
 		e = ast.NewBinExpr(token.OR, e, &ast.UnaryExpr{Op: token.MUL, X: s.default_})
 	}
+
+	if len(s.definitions) > 0 {
+		if st, ok := e.(*ast.StructLit); ok {
+			st.Elts = append(st.Elts, s.definitions...)
+		} else {
+			st = ast.NewStruct()
+			st.Elts = append(st.Elts, &ast.EmbedDecl{Expr: e})
+			st.Elts = append(st.Elts, s.definitions...)
+			e = st
+		}
+	}
+
+	s.linkReferences()
+
 	return e
 }
 
@@ -370,20 +411,22 @@
 	}
 }
 
-func (s *state) schema(n cue.Value) ast.Expr {
-	expr, _ := s.schemaState(n, allTypes, false)
+func (s *state) schema(n cue.Value, idRef ...label) ast.Expr {
+	expr, _ := s.schemaState(n, allTypes, idRef, false)
 	// TODO: report unused doc.
 	return expr
 }
 
 // schemaState is a low-level API for schema. isLogical specifies whether the
 // caller is a logical operator like anyOf, allOf, oneOf, or not.
-func (s *state) schemaState(n cue.Value, types cue.Kind, isLogical bool) (ast.Expr, *state) {
+func (s *state) schemaState(n cue.Value, types cue.Kind, idRef []label, isLogical bool) (ast.Expr, *state) {
 	state := &state{
+		up:           s,
 		isSchema:     s.isSchema,
 		decoder:      s.decoder,
 		allowedTypes: types,
 		path:         s.path,
+		idRef:        idRef,
 		pos:          n,
 	}
 	if isLogical {
diff --git a/encoding/jsonschema/decode_test.go b/encoding/jsonschema/decode_test.go
index a1c960e..39c09f4 100644
--- a/encoding/jsonschema/decode_test.go
+++ b/encoding/jsonschema/decode_test.go
@@ -102,12 +102,14 @@
 			if expr != nil {
 				b, err := format.Node(expr, format.Simplify())
 				if err != nil {
-					t.Fatal(err)
+					t.Fatal(errors.Details(err, nil))
 				}
 
 				// verify the generated CUE.
-				if _, err = r.Compile(fullpath, b); err != nil {
-					t.Fatal(errors.Details(err, nil))
+				if !bytes.Contains(a.Comment, []byte("#noverify")) {
+					if _, err = r.Compile(fullpath, b); err != nil {
+						t.Fatal(errors.Details(err, nil))
+					}
 				}
 
 				b = bytes.TrimSpace(b)
diff --git a/encoding/jsonschema/ref.go b/encoding/jsonschema/ref.go
index b0fa788..26443fe 100644
--- a/encoding/jsonschema/ref.go
+++ b/encoding/jsonschema/ref.go
@@ -17,8 +17,10 @@
 import (
 	"net/url"
 	"path"
+	"strconv"
 	"strings"
 
+	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
@@ -56,7 +58,340 @@
 	// means that %2F gets translated to `/` before it can be split. This, in
 	// turn, means that field names cannot have a `/` as name.
 
+	return splitFragment(u)
+}
+
+// resolveURI parses a URI from n and resolves it in the current context.
+// To resolve it in the current context, it looks for the closest URI from
+// an $id in the parent scopes and the uses the URI resolution to get the
+// new URI.
+//
+// This method is used to resolve any URI, including those from $id and $ref.
+func (s *state) resolveURI(n cue.Value) *url.URL {
+	str, ok := s.strValue(n)
+	if !ok {
+		return nil
+	}
+
+	u, err := url.Parse(str)
+	if err != nil {
+		s.addErr(errors.Newf(n.Pos(), "invalid JSON reference: %s", err))
+		return nil
+	}
+
+	for {
+		if s.id != nil {
+			u = s.id.ResolveReference(u)
+			break
+		}
+		if s.up == nil {
+			break
+		}
+		s = s.up
+	}
+
+	return u
+}
+
+const topSchema = "_schema"
+
+// makeCUERef converts a URI into a CUE reference for the current location.
+// The returned identifier (or first expression in a selection chain), is
+// hardwired to point to the resolved value. This will allow astutil.Sanitize
+// to automatically unshadow any shadowed variables.
+func (s *state) makeCUERef(n cue.Value, u *url.URL) ast.Expr {
+	a := splitFragment(u)
+
+	switch fn := s.cfg.Map; {
+	case fn != nil:
+		// TODO: This block is only used in case s.cfg.Map is set, which is
+		// currently only used for OpenAPI. Handling should be brought more in
+		// line with JSON schema.
+		a, err := fn(n.Pos(), a)
+		if err != nil {
+			s.addErr(errors.Newf(n.Pos(), "invalid reference %q: %v", u, err))
+			return nil
+		}
+		if len(a) == 0 {
+			// TODO: should we allow inserting at root level?
+			s.addErr(errors.Newf(n.Pos(),
+				"invalid empty reference returned by map for %q", u))
+			return nil
+		}
+		sel, ok := a[0].(ast.Expr)
+		if !ok {
+			sel = &ast.BadExpr{}
+		}
+		for _, l := range a[1:] {
+			switch x := l.(type) {
+			case *ast.Ident:
+				sel = &ast.SelectorExpr{X: sel, Sel: x}
+
+			case *ast.BasicLit:
+				sel = &ast.IndexExpr{X: sel, Index: x}
+			}
+		}
+		return sel
+	}
+
+	var ident *ast.Ident
+
+	for ; ; s = s.up {
+		if s.up == nil {
+			switch {
+			case u.Host == "" && u.Path == "",
+				s.id != nil && s.id.Host == u.Host && s.id.Path == u.Path:
+				if len(a) == 0 {
+					// refers to the top of the file. We will allow this by
+					// creating a helper schema as such:
+					//   _schema: {...}
+					//   _schema
+					// This is created at the finalization stage if
+					// hasSelfReference is set.
+					s.hasSelfReference = true
+
+					ident = ast.NewIdent(topSchema)
+					ident.Node = s.obj
+					return ident
+				}
+
+				ident, a = s.getNextIdent(n, a)
+
+			case u.Host != "":
+				// Reference not found within scope. Create an import reference.
+
+				// TODO: allow the configuration to specify a map from
+				// URI domain+paths to CUE packages.
+
+				// TODO: currently only $ids that are in scope can be
+				// referenced. We could consider doing an extra pass to record
+				// all '$id's in a file to be able to link to them even if they
+				// are not in scope.
+				p := u.Path
+
+				base := path.Base(p)
+				if !ast.IsValidIdent(base) {
+					if strings.HasSuffix(base, ".json") {
+						base = base[:len(base)-len(".json")]
+					}
+					if !ast.IsValidIdent(base) {
+						// Find something more clever to do there. For now just
+						// pick "schema" as the package name.
+						base = "schema"
+					}
+					p += ":" + base
+				}
+
+				ident = ast.NewIdent(base)
+				ident.Node = &ast.ImportSpec{Path: ast.NewString(u.Host + p)}
+
+			default:
+				// Just a path, not sure what that means.
+				s.errf(n, "unknown domain for reference %q", u)
+				return nil
+			}
+			break
+		}
+
+		if s.id == nil {
+			continue
+		}
+
+		if s.id.Host == u.Host && s.id.Path == u.Path {
+			if len(a) == 0 {
+				if len(s.idRef) == 0 {
+					// This is a reference to either root or a schema for which
+					// we do not yet support references. See Issue #386.
+					if s.up.up != nil {
+						s.errf(n, "cannot refer to internal schema %q", u)
+						return nil
+					}
+
+					// This is referring to the root scope. There is a dummy
+					// state above the root state that we need to update.
+					s = s.up
+
+					// refers to the top of the file. We will allow this by
+					// creating a helper schema as such:
+					//   _schema: {...}
+					//   _schema
+					// This is created at the finalization stage if
+					// hasSelfReference is set.
+					s.hasSelfReference = true
+					ident = ast.NewIdent(topSchema)
+					ident.Node = s.obj
+					return ident
+				}
+
+				x := s.idRef[0]
+				if !x.isDef && !ast.IsValidIdent(x.name) {
+					s.errf(n, "referring to field %q not supported", x.name)
+					return nil
+				}
+				e := ast.NewIdent(x.name)
+				if len(s.idRef) == 1 {
+					return e
+				}
+				return newSel(e, s.idRef[1])
+			}
+			ident, a = s.getNextIdent(n, a)
+			ident.Node = s.obj
+			break
+		}
+	}
+
+	return s.newSel(ident, n, a)
+}
+
+// getNextSelector translates a JSON Reference path into a CUE path by consuming
+// the first path elements and returning the corresponding CUE label.
+func (s *state) getNextSelector(v cue.Value, a []string) (l label, tail []string) {
+	switch elem := a[0]; elem {
+	case "$defs", "definitions":
+		if len(a) == 1 {
+			s.errf(v, "cannot refer to %s section: must refer to one of its elements", a[0])
+			return label{}, nil
+		}
+
+		if name := "#" + a[1]; ast.IsValidIdent(name) {
+			return label{name, true}, a[2:]
+		}
+
+		return label{"#", true}, a[1:]
+
+	case "properties":
+		if len(a) == 1 {
+			s.errf(v, "cannot refer to %s section: must refer to one of its elements", a[0])
+			return label{}, nil
+		}
+
+		return label{a[1], false}, a[2:]
+
+	default:
+		return label{elem, false}, a[1:]
+
+	case "additionalProperties",
+		"patternProperties",
+		"items",
+		"additionalItems":
+		// TODO: as a temporary workaround, include the schema verbatim.
+		// TODO: provide definitions for these in CUE.
+		s.errf(v, "referring to field %q not yet supported", elem)
+
+		// Other known fields cannot be supported.
+		return label{}, nil
+	}
+}
+
+// newSel converts a JSON Reference path and initial CUE identifier to
+// a CUE selection path.
+func (s *state) newSel(e ast.Expr, v cue.Value, a []string) ast.Expr {
+	for len(a) > 0 {
+		var label label
+		label, a = s.getNextSelector(v, a)
+		e = newSel(e, label)
+	}
+	return e
+}
+
+// newSel converts label to a CUE index and creates an expression to index
+// into e.
+func newSel(e ast.Expr, label label) ast.Expr {
+	if label.isDef {
+		return ast.NewSel(e, label.name)
+
+	}
+	if ast.IsValidIdent(label.name) && !internal.IsDefOrHidden(label.name) {
+		return ast.NewSel(e, label.name)
+	}
+	return &ast.IndexExpr{X: e, Index: ast.NewString(label.name)}
+}
+
+func (s *state) setField(lab label, f *ast.Field) {
+	x := s.getRef(lab)
+	x.field = f
+	s.setRef(lab, x)
+	x = s.getRef(lab)
+}
+
+func (s *state) getRef(lab label) refs {
+	if s.fieldRefs == nil {
+		s.fieldRefs = make(map[label]refs)
+	}
+	x, ok := s.fieldRefs[lab]
+	if !ok {
+		if lab.isDef ||
+			(ast.IsValidIdent(lab.name) && !internal.IsDefOrHidden(lab.name)) {
+			x.ident = lab.name
+		} else {
+			x.ident = "_X" + strconv.Itoa(s.decoder.numID)
+			s.decoder.numID++
+		}
+		s.fieldRefs[lab] = x
+	}
+	return x
+}
+
+func (s *state) setRef(lab label, r refs) {
+	s.fieldRefs[lab] = r
+}
+
+// getNextIdent gets the first CUE reference from a JSON Reference path and
+// converts it to a CUE identifier.
+func (s *state) getNextIdent(v cue.Value, a []string) (resolved *ast.Ident, tail []string) {
+	lab, a := s.getNextSelector(v, a)
+
+	x := s.getRef(lab)
+	ident := ast.NewIdent(x.ident)
+	x.refs = append(x.refs, ident)
+	s.setRef(lab, x)
+
+	return ident, a
+}
+
+// linkReferences resolves identifiers to relevant nodes. This allows
+// astutil.Sanitize to unshadow nodes if necessary.
+func (s *state) linkReferences() {
+	for _, r := range s.fieldRefs {
+		if r.field == nil {
+			// TODO: improve error message.
+			s.errf(cue.Value{}, "reference to non-existing value %q", r.ident)
+			continue
+		}
+
+		// link resembles the link value. See astutil.Resolve.
+		var link ast.Node
+
+		ident, ok := r.field.Label.(*ast.Ident)
+		if ok && ident.Name == r.ident {
+			link = r.field.Value
+		} else if len(r.refs) > 0 {
+			r.field.Label = &ast.Alias{
+				Ident: ast.NewIdent(r.ident),
+				Expr:  r.field.Label.(ast.Expr),
+			}
+			link = r.field
+		}
+
+		for _, i := range r.refs {
+			i.Node = link
+		}
+	}
+}
+
+// splitFragment splits the fragment part of a URI into path components. The
+// result may be an empty slice.
+//
+// TODO: this requires RawFragment introduced in go1.15 to function properly.
+// As for now, CUE still uses go1.12.
+func splitFragment(u *url.URL) []string {
+	if u.Fragment == "" {
+		return nil
+	}
 	s := strings.TrimRight(u.Fragment[1:], "/")
+	if s == "" {
+		return nil
+	}
 	return strings.Split(s, "/")
 }
 
diff --git a/encoding/jsonschema/testdata/def.txtar b/encoding/jsonschema/testdata/def.txtar
index 9b47661..35ea0a3 100644
--- a/encoding/jsonschema/testdata/def.txtar
+++ b/encoding/jsonschema/testdata/def.txtar
@@ -39,7 +39,8 @@
 }
 
 -- out.cue --
-@jsonschema(schema="http://json-schema.org/draft-07/schema#",id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
+@jsonschema(schema="http://json-schema.org/draft-07/schema#")
+@jsonschema(id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
 person?:           #["per-son"]
 billing_address?:  #address
 shipping_address?: #address
diff --git a/encoding/jsonschema/testdata/ref.txtar b/encoding/jsonschema/testdata/ref.txtar
new file mode 100644
index 0000000..50d56ab
--- /dev/null
+++ b/encoding/jsonschema/testdata/ref.txtar
@@ -0,0 +1,111 @@
+// This test tests the conversion and ordering of $defs.
+
+#noverify
+
+-- definition.json --
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+
+  "$id": "http://cuelang.org/go/encoding/openapi/testdata/order.json",
+
+  "$defs": {
+    "address": {
+      "type": "object",
+      "properties": {
+        "city": { "type": "string" }
+      }
+    },
+    "int": {
+      "type": "integer"
+    },
+    "string-int": {
+      "type": [ "integer", "string" ]
+    },
+    "person": {
+      "type": "object",
+      "properties": {
+        "name": { "type": "string" },
+        "children": {
+          "type": "object",
+          "properties": {
+              "x": { "$ref": "http://acme.com/external.json#/properties/foo" },
+
+              "a": { "$ref": "#/$defs/int" },
+              "b": { "$ref": "http://cuelang.org/person.json#/$defs/int" },
+              "c": { "$ref": "http://cuelang.org/go/encoding/openapi/testdata/order.json#/$defs/int" },
+              "d": { "$ref": "http://cuelang.org/go/encoding/openapi/testdata/order.json#/$defs/address" },
+              "e": { "$ref": "http://cuelang.org/go/encoding/openapi/testdata/order.json#/$defs/string-int" },
+              "f": { "$ref": "http://cuelang.org/person.json" },
+              "g": { "$ref": "http://acme.com/external.json#/definitions/foo" },
+              "h": { "$ref": "http://acme.com/external.json#/properties/foo" },
+              "i": { "$ref": "http://acme.com/external.json" },
+              "j": { "$ref": "http://acme.com/external-foo.json" },
+              "k": { "$ref": "http://acme.com/external-bar.json" },
+              "z": {}
+          }
+        }
+      },
+      "$id": "http://cuelang.org/person.json",
+      "$defs": {
+        "int": {
+          "type": "integer"
+        }
+      }
+    }
+  },
+
+  "type": "object",
+
+  "properties": {
+    "person": { "$ref": "#/$defs/person" },
+    "billing_address": { "$ref": "#/$defs/address" },
+    "shipping_address": { "$ref": "#/$defs/address" }
+  }
+}
+
+-- out.cue --
+import (
+	"acme.com/external.json:external"
+	"acme.com/external-foo.json:schema"
+	schema_5 "acme.com/external-bar.json:schema"
+)
+
+@jsonschema(schema="http://json-schema.org/draft-07/schema#")
+@jsonschema(id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
+person?:           #person
+billing_address?:  #address
+shipping_address?: #address
+
+#int_1=#int: int
+
+#address: {
+	city?: string
+	...
+}
+
+#: "string-int": int | string
+
+#person: {
+	@jsonschema(id="http://cuelang.org/person.json")
+	name?: string
+	children?: {
+		x?: external.foo
+		a?: #int
+		b?: #int
+		c?: #int_1
+		d?: #address
+		e?: #["string-int"]
+		f?: #person
+		g?: external.#foo
+		h?: external.foo
+		i?: external
+		j?: schema
+		k?: schema_5
+		z?: _
+		...
+	}
+
+	#int: int
+	...
+}
+...
diff --git a/encoding/jsonschema/testdata/refroot.txtar b/encoding/jsonschema/testdata/refroot.txtar
new file mode 100644
index 0000000..c090506
--- /dev/null
+++ b/encoding/jsonschema/testdata/refroot.txtar
@@ -0,0 +1,27 @@
+// This test tests the conversion and ordering of $defs.
+
+-- definition.json --
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+
+  "$id": "http://cuelang.org/go/encoding/openapi/testdata/order.json",
+
+  "properties": {
+    "value": {},
+    "next": { "$ref": "#" }
+  }
+}
+
+-- out.cue --
+_schema
+_schema: {
+	@jsonschema(schema="http://json-schema.org/draft-07/schema#")
+	number | null | bool | string | [...] | {
+		@jsonschema(id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
+		value?: _
+		next?:  _schema_1
+		...
+	}
+}
+
+let _schema_1 = _schema
diff --git a/encoding/jsonschema/testdata/refroot2.txtar b/encoding/jsonschema/testdata/refroot2.txtar
new file mode 100644
index 0000000..20e6361
--- /dev/null
+++ b/encoding/jsonschema/testdata/refroot2.txtar
@@ -0,0 +1,24 @@
+// This test tests the conversion and ordering of $defs.
+
+-- definition.json --
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+
+  "properties": {
+    "value": {},
+    "next": { "$ref": "#" }
+  }
+}
+
+-- out.cue --
+_schema
+_schema: {
+	@jsonschema(schema="http://json-schema.org/draft-07/schema#")
+	number | null | bool | string | [...] | {
+		value?: _
+		next?:  _schema_1
+		...
+	}
+}
+
+let _schema_1 = _schema