encoding/jsonschema: use new definition mapping

This allows JSON Schema to now directly be compared to
data without qualifiers or remapping:

   $ cue eval schema.json data.yaml

This works now more intuively as data and schema now map
one-to-one.

This relies on the change to allow `...` at the file level. This means
it is not necessary to use the new `isSchema` field. It seems
sensible to track this though and we may need it in the future.

One huge caveat of this new mapping is that it is now no longer
possible to have a non-struct schema with definitions remapped
to a different label. For this we need to allow embedded scalars in
structs. It still seems worth it and rather to allow this in the language.

Change-Id: I16988ca1c0d4436d591ba239964c4bb7445e6fd9
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5942
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/testdata/script/def_jsonschema.txt b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
index 96e665f..5c6224c 100644
--- a/cmd/cue/cmd/testdata/script/def_jsonschema.txt
+++ b/cmd/cue/cmd/testdata/script/def_jsonschema.txt
@@ -1,8 +1,8 @@
-cue def jsonschema: schema.json -p schema -l 'Person::'
+cue def jsonschema: schema.json -p schema -l '#Person:'
 cmp stdout expect-stdout
 
 # auto mode
-cue def schema.json -p schema -l 'Person::'
+cue def schema.json -p schema -l '#Person:'
 cmp stdout expect-stdout
 
 cue def jsonschema: bad.json
@@ -10,22 +10,22 @@
 ! cue def jsonschema: bad.json --strict
 cmp stderr expect-stderr
 
+! cue eval data.yaml schema.json
+cmp stderr expect-stderr2
+
 -- expect-stdout --
 package schema
 
-Person :: {
-	// Person
-	Schema :: {
-		// The person's first name.
-		firstName?: string
+#Person: {
+	// The person's first name.
+	firstName?: string
 
-		// The person's last name.
-		lastName?: string
+	// The person's last name.
+	lastName?: string
 
-		// Age in years which must be equal to or greater than zero.
-		age?: >=0
-		...
-	} @jsonschema(schema="http://json-schema.org/draft-07/schema#",id="https://example.com/person.schema.json")
+	// Age in years which must be equal to or greater than zero.
+	age?: >=0
+	...
 }
 -- schema.json --
 {
@@ -55,8 +55,14 @@
   "type": "number",
   "foo": "bar"
 }
-
 -- expect-stderr --
 unsupported constraint "foo":
     ./bad.json:3:10
+-- data.yaml --
+age: twenty
+
+-- expect-stderr2 --
+age: conflicting values "twenty" and >=0 (mismatched types string and number):
+    11:7
+    ./data.yaml:1:7
 -- cue.mod --
diff --git a/cmd/cue/cmd/testdata/script/def_openapi.txt b/cmd/cue/cmd/testdata/script/def_openapi.txt
index 500b88e..d25aa19 100644
--- a/cmd/cue/cmd/testdata/script/def_openapi.txt
+++ b/cmd/cue/cmd/testdata/script/def_openapi.txt
@@ -212,11 +212,11 @@
 	version: *"v1alpha1" | string
 }
 
-Bar :: {
-	foo: Foo
+#Bar: {
+	foo: #Foo
 	...
 }
-Foo :: {
+#Foo: {
 	a: int
 	b: >=0 & <10
 	...
@@ -226,11 +226,11 @@
 	title:   *"Some clever title." | string
 	version: *"v1" | string
 }
-Bar :: {
-	foo: Foo
+#Bar: {
+	foo: #Foo
 	...
 }
-Foo :: {
+#Foo: {
 	a: int
 	b: >=0 & <10
 	...
@@ -240,11 +240,11 @@
 	title:   *"Some clever title." | string
 	version: *"v1" | string
 }
-Bar :: {
-	foo: Foo
+#Bar: {
+	foo: #Foo
 	...
 }
-Foo :: {
+#Foo: {
 	a: int
 	b: >=0 & <10
 	...
@@ -257,16 +257,16 @@
 	title:   string | *_|_
 	version: *"v1alpha1" | string
 }
-Bar :: {
-	foo: Foo
+#Bar: {
+	foo: #Foo
 	...
 }
-Foo :: {
+#Foo: {
 	a: int
 	b: >=0 & <10
 	...
 }
-Baz :: {
+#Baz: {
 	a: int
 	b: >=0 & <10
 	...
diff --git a/cmd/cue/cmd/testdata/script/import_auto.txt b/cmd/cue/cmd/testdata/script/import_auto.txt
index bef7dab..3a87889 100644
--- a/cmd/cue/cmd/testdata/script/import_auto.txt
+++ b/cmd/cue/cmd/testdata/script/import_auto.txt
@@ -9,13 +9,13 @@
 	version: *"v1beta1" | string
 }
 
-Foo :: {
+#Foo: {
 	a: int
 	b: >=0 & <10
 	...
 }
-Bar :: {
-	foo: Foo
+#Bar: {
+	foo: #Foo
 	...
 }
 -- openapi.yaml --
diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go
index b41b798..2ad9a90 100644
--- a/encoding/jsonschema/constraints.go
+++ b/encoding/jsonschema/constraints.go
@@ -78,16 +78,19 @@
 		s.errf(n, `"definitions" expected an object, found %v`, n.Kind)
 	}
 
+	old := s.isSchema
+	s.isSchema = true
+	defer func() { s.isSchema = old }()
+
 	s.processMap(n, func(key string, n cue.Value) {
-		f := &ast.Field{
-			Label: ast.NewString(s.path[len(s.path)-1]),
-			Token: token.ISA,
-			Value: s.schema(n),
+		name := s.path[len(s.path)-1]
+		a, _ := jsonSchemaRef(n.Pos(), []string{"definitions", name})
+
+		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)}
 		}
-		f = &ast.Field{
-			Label: ast.NewIdent(rootDefs),
-			Value: ast.NewStruct(f),
-		}
+
 		ast.SetRelPos(f, token.NewSection)
 		s.definitions = append(s.definitions, f)
 	})
@@ -211,15 +214,29 @@
 	p0("$ref", func(n cue.Value, s *state) {
 		s.usedTypes = allTypes
 		str, _ := s.strValue(n)
-		a := s.parseRef(n.Pos(), str)
-		if a != nil {
-			a = s.mapRef(n.Pos(), str, a)
+		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()})
 			return
 		}
-		s.addConjunct(ast.NewSel(ast.NewIdent(a[0]), a[1:]...))
+		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}
+			}
+		}
+		s.addConjunct(sel)
 	}),
 
 	// Combinators
@@ -384,8 +401,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: ast.NewString(key), Value: expr}
+			f := &ast.Field{Label: label, Value: expr}
 			state.doc(f)
 			f.Optional = token.Blank.Pos()
 			if len(s.obj.Elts) > 0 && len(f.Comments()) > 0 {
@@ -396,7 +414,7 @@
 			if state.deprecated {
 				switch expr.(type) {
 				case *ast.StructLit:
-					s.obj.Elts = append(s.obj.Elts, addTag(key, "deprecated", ""))
+					s.obj.Elts = append(s.obj.Elts, addTag(label, "deprecated", ""))
 				default:
 					f.Attrs = append(f.Attrs, internal.NewAttr("deprecated", ""))
 				}
diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go
index db9007d..70d4338 100644
--- a/encoding/jsonschema/decode.go
+++ b/encoding/jsonschema/decode.go
@@ -31,7 +31,11 @@
 	"cuelang.org/go/internal"
 )
 
-const rootDefs = "def"
+// rootDefs defines the top-level name of the map of definitions that do not
+// have a valid identifier name.
+//
+// TODO: find something more principled, like allowing #."a-b" or `#a-b`.
+const rootDefs = "#def"
 
 // A decoder converts JSON schema to CUE.
 type decoder struct {
@@ -64,7 +68,7 @@
 	var a []ast.Decl
 
 	if d.cfg.Root == "" {
-		a = append(a, d.schema([]string{"Schema"}, v)...)
+		a = append(a, d.schema(nil, v)...)
 	} else {
 		ref := d.parseRef(token.NoPos, d.cfg.Root)
 		if ref == nil {
@@ -77,11 +81,11 @@
 		}
 		for i.Next() {
 			ref := append(ref, i.Label())
-			ref = d.mapRef(i.Value().Pos(), "", ref)
-			if len(ref) == 0 {
+			lab := d.mapRef(i.Value().Pos(), "", ref)
+			if len(lab) == 0 {
 				return nil
 			}
-			decls := d.schema(ref, i.Value())
+			decls := d.schema(lab, i.Value())
 			a = append(a, decls...)
 		}
 	}
@@ -106,11 +110,16 @@
 	return f
 }
 
-func (d *decoder) schema(ref []string, v cue.Value) (a []ast.Decl) {
+func (d *decoder) schema(ref []ast.Label, v cue.Value) (a []ast.Decl) {
 	root := state{decoder: d}
 
+	var name ast.Label
 	inner := len(ref) - 1
-	name := ref[inner]
+
+	if inner >= 0 {
+		name = ref[inner]
+		root.isSchema = true
+	}
 
 	expr, state := root.schemaState(v, allTypes, false)
 
@@ -121,30 +130,48 @@
 	if state.id != "" {
 		tags = append(tags, fmt.Sprintf("id=%q", state.id))
 	}
-	if len(tags) > 0 {
-		a = append(a, addTag(name, "jsonschema", strings.Join(tags, ",")))
+
+	if name == nil {
+		if len(tags) > 0 {
+			body := strings.Join(tags, ",")
+			a = append(a, &ast.Attribute{
+				Text: fmt.Sprintf("@jsonschema(%s)", body)})
+		}
+
+		if state.deprecated {
+			a = append(a, &ast.Attribute{Text: "@deprecated()"})
+		}
+	} else {
+		if len(tags) > 0 {
+			a = append(a, addTag(name, "jsonschema", strings.Join(tags, ",")))
+		}
+
+		if state.deprecated {
+			a = append(a, addTag(name, "deprecated", ""))
+		}
 	}
 
-	if state.deprecated {
-		a = append(a, addTag(name, "deprecated", ""))
+	if name != nil {
+		f := &ast.Field{
+			Label: name,
+			Value: expr,
+		}
+
+		a = append(a, f)
+	} else if st, ok := expr.(*ast.StructLit); ok {
+		a = append(a, st.Elts...)
+	} else {
+		a = append(a, &ast.EmbedDecl{Expr: expr})
 	}
 
-	f := &ast.Field{
-		Label: ast.NewIdent(name),
-		Token: token.ISA,
-		Value: expr,
-	}
-
-	a = append(a, f)
 	state.doc(a[0])
 
 	for i := inner - 1; i >= 0; i-- {
 		a = []ast.Decl{&ast.Field{
-			Label: ast.NewIdent(ref[i]),
-			Token: token.ISA,
+			Label: ref[i],
 			Value: &ast.StructLit{Elts: a},
 		}}
-		expr = ast.NewStruct(ref[i], token.ISA, expr)
+		expr = ast.NewStruct(ref[i], expr)
 	}
 
 	return a
@@ -205,6 +232,8 @@
 type state struct {
 	*decoder
 
+	isSchema bool // for omitting ellipsis in an ast.File
+
 	parent *state
 
 	path []string
@@ -285,6 +314,7 @@
 	conjuncts = append(conjuncts, s.conjuncts...)
 
 	if s.obj != nil {
+		// TODO: may need to explicitly close.
 		if !s.closeStruct {
 			s.obj.Elts = append(s.obj.Elts, &ast.Ellipsis{})
 		}
@@ -360,6 +390,7 @@
 // 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) {
 	state := &state{
+		isSchema:     s.isSchema,
 		decoder:      s.decoder,
 		allowedTypes: types,
 		path:         s.path,
@@ -414,6 +445,7 @@
 				Value: s.value(n),
 			})
 		})
+		// TODO: only open when s.isSchema?
 		a = append(a, &ast.Ellipsis{})
 		return setPos(&ast.StructLit{Elts: a}, n)
 
@@ -485,10 +517,9 @@
 	return &ast.UnaryExpr{Op: token.NMAT, X: ast.NewString(re)}
 }
 
-func addTag(field, tag, value string) *ast.Field {
+func addTag(field ast.Label, tag, value string) *ast.Field {
 	return &ast.Field{
-		Label: ast.NewIdent(field),
-		Token: token.ISA,
+		Label: field,
 		Value: ast.NewIdent("_"),
 		Attrs: []*ast.Attribute{
 			{Text: fmt.Sprintf("@%s(%s)", tag, value)},
diff --git a/encoding/jsonschema/decode_test.go b/encoding/jsonschema/decode_test.go
index 469bbb5..a1c960e 100644
--- a/encoding/jsonschema/decode_test.go
+++ b/encoding/jsonschema/decode_test.go
@@ -25,6 +25,7 @@
 	"testing"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/format"
 	"cuelang.org/go/cue/token"
@@ -58,9 +59,9 @@
 
 			if bytes.Contains(a.Comment, []byte("openapi")) {
 				cfg.Root = "#/components/schemas/"
-				cfg.Map = func(p token.Pos, a []string) ([]string, error) {
+				cfg.Map = func(p token.Pos, a []string) ([]ast.Label, error) {
 					// Just for testing: does not validate the path.
-					return []string{a[len(a)-1]}, nil
+					return []ast.Label{ast.NewIdent("#" + a[len(a)-1])}, nil
 				}
 			}
 
diff --git a/encoding/jsonschema/jsonschema.go b/encoding/jsonschema/jsonschema.go
index 4a59ab1..398cbbf 100644
--- a/encoding/jsonschema/jsonschema.go
+++ b/encoding/jsonschema/jsonschema.go
@@ -69,13 +69,14 @@
 	Root string
 
 	// Map maps the locations of schemas and definitions to a new location.
-	// References are updated accordingly.
+	// References are updated accordingly. A returned label must be
+	// an identifier or string literal.
 	//
 	// The default mapping is
-	//    {}                     {"Schema"}
-	//    {"definitions", foo}   {"Defs", strings.Title(foo)}
-	//    {"$defs", foo}         {"Defs", strings.Title(foo)}
-	Map func(pos token.Pos, path []string) ([]string, error)
+	//    {}                     {}
+	//    {"definitions", foo}   {#foo} or {#def, foo}
+	//    {"$defs", foo}         {#foo} or {#def, foo}
+	Map func(pos token.Pos, path []string) ([]ast.Label, error)
 
 	// TODO: configurability to make it compatible with OpenAPI, such as
 	// - locations of definitions: #/components/schemas, for instance.
diff --git a/encoding/jsonschema/ref.go b/encoding/jsonschema/ref.go
index 6961a2e..59c2130 100644
--- a/encoding/jsonschema/ref.go
+++ b/encoding/jsonschema/ref.go
@@ -19,6 +19,7 @@
 	"path"
 	"strings"
 
+	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
 )
@@ -58,7 +59,7 @@
 	return strings.Split(s, "/")
 }
 
-func (d *decoder) mapRef(p token.Pos, str string, ref []string) []string {
+func (d *decoder) mapRef(p token.Pos, str string, ref []string) []ast.Label {
 	fn := d.cfg.Map
 	if fn == nil {
 		fn = jsonSchemaRef
@@ -83,7 +84,7 @@
 	return a
 }
 
-func jsonSchemaRef(p token.Pos, a []string) ([]string, error) {
+func jsonSchemaRef(p token.Pos, a []string) ([]ast.Label, error) {
 	// TODO: technically, references could reference a
 	// non-definition. We disallow this case for the standard
 	// JSON Schema interpretation. We could detect cases that
@@ -95,5 +96,12 @@
 			// to already have been withdrawn from the JSON Schema spec.
 			"$ref must be of the form #/definitions/...")
 	}
-	return append([]string{rootDefs}, a[1:]...), nil
+	name := a[1]
+	if ast.IsValidIdent(name) &&
+		name != rootDefs[1:] &&
+		!strings.HasPrefix(name, "#") &&
+		!strings.HasPrefix(name, "_") {
+		return []ast.Label{ast.NewIdent("#" + name)}, nil
+	}
+	return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil
 }
diff --git a/encoding/jsonschema/testdata/basic.txtar b/encoding/jsonschema/testdata/basic.txtar
index ab9d762..d2308b3 100644
--- a/encoding/jsonschema/testdata/basic.txtar
+++ b/encoding/jsonschema/testdata/basic.txtar
@@ -40,20 +40,19 @@
 // Main schema
 //
 // Specify who you are and all.
-Schema :: _ @jsonschema(schema="http://json-schema.org/draft-07/schema#")
-Schema :: {
-	// A person is a human being.
-	person?: {
-		name: string
+@jsonschema(schema="http://json-schema.org/draft-07/schema#")
 
-		// where does this person live?
-		address?: strings.MinRunes(4) & strings.MaxRunes(20)
+// A person is a human being.
+person?: {
+	name: string
 
-		// A very large comment that will be wrapped after a certain line
-		// length. Let's keep on going and see what happens.
-		children?: [...string]
-		"home phone"?: string @deprecated()
-		...
-	}
+	// where does this person live?
+	address?: strings.MinRunes(4) & strings.MaxRunes(20)
+
+	// A very large comment that will be wrapped after a certain line
+	// length. Let's keep on going and see what happens.
+	children?: [...string]
+	"home phone"?: string @deprecated()
 	...
 }
+...
diff --git a/encoding/jsonschema/testdata/def.txtar b/encoding/jsonschema/testdata/def.txtar
index e8836fa..064c4e7 100644
--- a/encoding/jsonschema/testdata/def.txtar
+++ b/encoding/jsonschema/testdata/def.txtar
@@ -16,13 +16,13 @@
       },
       "required": ["street_address", "city", "state"]
     },
-    "person": {
+    "per-son": {
       "type": "object",
       "properties": {
         "name": { "type": "string" },
         "children": {
           "type": "array",
-          "items": { "$ref": "#/definitions/person" },
+          "items": { "$ref": "#/definitions/per-son" },
           "default": []
         }
       }
@@ -32,30 +32,28 @@
   "type": "object",
 
   "properties": {
-    "person": { "$ref": "#/definitions/person" },
+    "person": { "$ref": "#/definitions/per-son" },
     "billing_address": { "$ref": "#/definitions/address" },
     "shipping_address": { "$ref": "#/definitions/address" }
   }
 }
 
 -- out.cue --
-Schema :: _ @jsonschema(schema="http://json-schema.org/draft-07/schema#",id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
-Schema :: {
-	person?:           def.person
-	billing_address?:  def.address
-	shipping_address?: def.address
-	...
-}
+@jsonschema(schema="http://json-schema.org/draft-07/schema#",id="http://cuelang.org/go/encoding/openapi/testdata/order.json")
+person?:           #def["per-son"]
+billing_address?:  #address
+shipping_address?: #address
 
-def: address :: {
+#address: {
 	street_address: string
 	city:           string
 	state:          string
 	...
 }
 
-def: person :: {
+#def: "per-son": {
 	name?: string
-	children?: [...def.person]
+	children?: [...#def["per-son"]]
 	...
 }
+...
diff --git a/encoding/jsonschema/testdata/list.txtar b/encoding/jsonschema/testdata/list.txtar
index a746e9c..3f7bd0a 100644
--- a/encoding/jsonschema/testdata/list.txtar
+++ b/encoding/jsonschema/testdata/list.txtar
@@ -38,10 +38,8 @@
 -- out.cue --
 import "list"
 
-Schema :: {
-	foo?: [...string]
-	tuple?: [string, int, 2]
-	has?:  list.Contains(3)
-	size?: [_, _, _, ...] & list.MaxItems(9) & list.UniqueItems()
-	additional?: [int, int, ...string]
-}
+foo?: [...string]
+tuple?: [string, int, 2]
+has?:  list.Contains(3)
+size?: [_, _, _, ...] & list.MaxItems(9) & list.UniqueItems()
+additional?: [int, int, ...string]
diff --git a/encoding/jsonschema/testdata/num.txtar b/encoding/jsonschema/testdata/num.txtar
index a7e028a..1968c3a 100644
--- a/encoding/jsonschema/testdata/num.txtar
+++ b/encoding/jsonschema/testdata/num.txtar
@@ -28,10 +28,8 @@
 -- out.cue --
 import "math"
 
-Schema :: {
-	constant?:  2
-	several?:   1 | 2 | 3 | 4
-	inclusive?: >=2 & <=3
-	exclusive?: >2 & <3
-	cents?:     math.MultipleOf(0.05)
-}
+constant?:  2
+several?:   1 | 2 | 3 | 4
+inclusive?: >=2 & <=3
+exclusive?: >2 & <3
+cents?:     math.MultipleOf(0.05)
diff --git a/encoding/jsonschema/testdata/object.txtar b/encoding/jsonschema/testdata/object.txtar
index f25256e..9f773e1 100644
--- a/encoding/jsonschema/testdata/object.txtar
+++ b/encoding/jsonschema/testdata/object.txtar
@@ -62,40 +62,38 @@
 import "struct"
 
 // Main schema
-Schema :: {
-	fields?: struct.MaxFields(10) & {
-		[=~"^\\P{Lu}"]: _
-	}
-	additional?: {
-		foo?: number
-		bar?: number
+fields?: struct.MaxFields(10) & {
+	[=~"^\\P{Lu}"]: _
+}
+additional?: {
+	foo?: number
+	bar?: number
 
-		{[!~"^(foo|bar)$"]: string}
-	}
-	map?: [string]: string
-	patterns?: {
-		foo?: number
-		bar?: number
+	{[!~"^(foo|bar)$"]: string}
+}
+map?: [string]: string
+patterns?: {
+	foo?: number
+	bar?: number
 
-		{[=~"^\\P{Lu}" & !~"^(foo|bar)$"]: string}
+	{[=~"^\\P{Lu}" & !~"^(foo|bar)$"]: string}
 
-		{[=~"^\\P{Lo}" & !~"^(foo|bar)$"]: int}
-		...
-	}
-	patternsNoProps?: {
-		{[=~"^\\P{Lu}" & !~"^()$"]: string}
+	{[=~"^\\P{Lo}" & !~"^(foo|bar)$"]: int}
+	...
+}
+patternsNoProps?: {
+	{[=~"^\\P{Lu}" & !~"^()$"]: string}
 
-		{[=~"^\\P{Lo}" & !~"^()$"]: int}
-		...
-	}
-	complex?: {
-		foo?: number
-		bar?: number
+	{[=~"^\\P{Lo}" & !~"^()$"]: int}
+	...
+}
+complex?: {
+	foo?: number
+	bar?: number
 
-		{[=~"^\\P{Lu}" & !~"^(foo|bar)$"]: string}
+	{[=~"^\\P{Lu}" & !~"^(foo|bar)$"]: string}
 
-		{[=~"^\\P{Lo}" & !~"^(foo|bar)$"]: int}
+	{[=~"^\\P{Lo}" & !~"^(foo|bar)$"]: int}
 
-		{[!~"^\\P{Lu}" & !~"^\\P{Lo}" & !~"^(foo|bar)$"]: string}
-	}
+	{[!~"^\\P{Lu}" & !~"^\\P{Lo}" & !~"^(foo|bar)$"]: string}
 }
diff --git a/encoding/jsonschema/testdata/openapi.txtar b/encoding/jsonschema/testdata/openapi.txtar
index 139cd5b..ad52ca1 100644
--- a/encoding/jsonschema/testdata/openapi.txtar
+++ b/encoding/jsonschema/testdata/openapi.txtar
@@ -19,12 +19,12 @@
 
 -- out.cue --
 // A User uses something.
-User :: {
+#User: {
 	name?:    string
 	id?:      int
-	address?: PhoneNumber
+	address?: #PhoneNumber
 	...
 }
 
 // The number to dial.
-PhoneNumber :: string
+#PhoneNumber: string
diff --git a/encoding/jsonschema/testdata/type.txtar b/encoding/jsonschema/testdata/type.txtar
index e851653..b2be8f7 100644
--- a/encoding/jsonschema/testdata/type.txtar
+++ b/encoding/jsonschema/testdata/type.txtar
@@ -31,15 +31,12 @@
 
 -- out.cue --
 // Main schema
-Schema :: {
-	// an integer or string.
-	intString?: null | bool | int | string | [...]
-	object?:    {
-		...
-	} | *{
-		foo: "bar"
-		baz: 1.3
-		...
-	}
-	numOrList?: number | [...number] | *[1, 2, 3]
+intString?: null | bool | int | string | [...]
+object?:    {
+	...
+} | *{
+	foo: "bar"
+	baz: 1.3
+	...
 }
+numOrList?: number | [...number] | *[1, 2, 3]
diff --git a/encoding/jsonschema/testdata/typedis.txtar b/encoding/jsonschema/testdata/typedis.txtar
index d718d0c..6bf10ac 100644
--- a/encoding/jsonschema/testdata/typedis.txtar
+++ b/encoding/jsonschema/testdata/typedis.txtar
@@ -46,11 +46,9 @@
 }
 -- out.cue --
 // Main schema
-Schema :: {
-	intOrString1?: int | string
-	intOrString2?: int | string
-	intOrString3?: int | string
-	disjunction?:  int | string | >=3
-	empty?:        _|_
-	...
-}
+intOrString1?: int | string
+intOrString2?: int | string
+intOrString3?: int | string
+disjunction?:  int | string | >=3
+empty?:        _|_
+...
diff --git a/encoding/jsonschema/testdata/unsupported.txtar b/encoding/jsonschema/testdata/unsupported.txtar
index b2b3449..a6f8f36 100644
--- a/encoding/jsonschema/testdata/unsupported.txtar
+++ b/encoding/jsonschema/testdata/unsupported.txtar
@@ -35,10 +35,10 @@
 
 
 -- out.cue --
-Schema :: _ @jsonschema(schema="http://json-schema.org/draft-07/schema")
-Schema :: _
+@jsonschema(schema="http://json-schema.org/draft-07/schema")
+_
 
-def: ref :: null | {
+#ref: null | {
 	branches?: {
 		...
 	}
diff --git a/encoding/openapi/decode.go b/encoding/openapi/decode.go
index 8f24dbe..05a27c3 100644
--- a/encoding/openapi/decode.go
+++ b/encoding/openapi/decode.go
@@ -122,11 +122,22 @@
 
 const oapiSchemas = "#/components/schemas/"
 
-func openAPIMapping(pos token.Pos, a []string) ([]string, error) {
+// rootDefs is the fallback for schemas that are not valid identifiers.
+// TODO: find something more principled.
+const rootDefs = "#SchemaMap"
+
+func openAPIMapping(pos token.Pos, a []string) ([]ast.Label, error) {
 	if len(a) != 3 || a[0] != "components" || a[1] != "schemas" {
 		return nil, errors.Newf(pos,
 			`openapi: reference must be of the form %q; found "#/%s"`,
 			oapiSchemas, strings.Join(a, "/"))
 	}
-	return a[2:], nil
+	name := a[2]
+	if ast.IsValidIdent(name) &&
+		name != rootDefs[1:] &&
+		!strings.HasPrefix(name, "#") &&
+		!strings.HasPrefix(name, "_") {
+		return []ast.Label{ast.NewIdent("#" + name)}, nil
+	}
+	return []ast.Label{ast.NewIdent(rootDefs), ast.NewString(name)}, nil
 }
diff --git a/encoding/openapi/testdata/script/basics.txtar b/encoding/openapi/testdata/script/basics.txtar
index 4056564..6b63b02 100644
--- a/encoding/openapi/testdata/script/basics.txtar
+++ b/encoding/openapi/testdata/script/basics.txtar
@@ -36,12 +36,12 @@
 	}
 }
 // A User uses something.
-User :: {
+#User: {
 	name?:    string
 	id?:      int
-	address?: PhoneNumber
+	address?: #PhoneNumber
 	...
 }
 
 // The number to dial.
-PhoneNumber :: string
+#PhoneNumber: string