encoding/openapi: implement structural schema

See https://kubernetes.io/blog/2019/06/20/crd-structural-schema/

This is needed to make generated schema compliant with CRDs.

Structural schema are momentarily enabled by requesting to
expand references. Even when not expanding, the generator
will strive to normalize the schema somewhat, however.

Change-Id: I36fc8bc0d0e41d1b47b8bed55462ab9d07cfc26f
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2803
Reviewed-by: Jason Wang <jasonwzm@google.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index c491a1c..6a32f5d 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -36,6 +36,7 @@
 	path      []string
 
 	expandRefs  bool
+	structural  bool
 	nameFunc    func(inst *cue.Instance, path []string) string
 	descFunc    func(v cue.Value) string
 	fieldFilter *regexp.Regexp
@@ -80,6 +81,7 @@
 		inst:         inst,
 		refPrefix:    "components/schemas",
 		expandRefs:   g.ExpandReferences,
+		structural:   g.ExpandReferences,
 		nameFunc:     g.ReferenceFunc,
 		descFunc:     g.DescriptionFunc,
 		schemas:      &OrderedMap{},
@@ -137,7 +139,7 @@
 }
 
 func (c *buildContext) build(name string, v cue.Value) *oaSchema {
-	return newRootBuilder(c).schema(name, v)
+	return newCoreBuilder(c).schema(nil, name, v)
 }
 
 // isInternal reports whether or not to include this type.
@@ -168,37 +170,67 @@
 	}
 }
 
-func (b *builder) schema(name string, v cue.Value) *oaSchema {
+func (b *builder) schema(core *builder, name string, v cue.Value) *oaSchema {
 	oldPath := b.ctx.path
 	b.ctx.path = append(b.ctx.path, name)
 	defer func() { b.ctx.path = oldPath }()
 
-	c := newRootBuilder(b.ctx)
-	c.format = extractFormat(v)
-	isRef := c.value(v, nil)
-	schema := c.finish()
+	var c *builder
+	if core == nil && b.ctx.structural {
+		c = newCoreBuilder(b.ctx)
+		c.buildCore(v)     // initialize core structure
+		c.coreSchema(name) // build the
+	} else {
+		c = newRootBuilder(b.ctx)
+		c.core = core
+	}
 
-	if !isRef {
-		doc := []string{}
-		if b.ctx.descFunc != nil {
-			if str := b.ctx.descFunc(v); str != "" {
-				doc = append(doc, str)
-			}
-		} else {
-			for _, d := range v.Doc() {
-				doc = append(doc, d.Text())
-			}
+	return c.fillSchema(v)
+}
+
+func (b *builder) getDoc(v cue.Value) {
+	doc := []string{}
+	if b.ctx.descFunc != nil {
+		if str := b.ctx.descFunc(v); str != "" {
+			doc = append(doc, str)
 		}
-		if len(doc) > 0 {
-			str := strings.TrimSpace(strings.Join(doc, "\n\n"))
-			schema.Set("description", str)
+	} else {
+		for _, d := range v.Doc() {
+			doc = append(doc, d.Text())
+		}
+	}
+	if len(doc) > 0 {
+		str := strings.TrimSpace(strings.Join(doc, "\n\n"))
+		b.setSingle("description", str, true)
+	}
+}
+
+func (b *builder) fillSchema(v cue.Value) *oaSchema {
+	if b.filled != nil {
+		return b.filled
+	}
+
+	b.setValueType(v)
+	b.format = extractFormat(v)
+
+	if b.core == nil || len(b.core.values) > 1 {
+		isRef := b.value(v, nil)
+		if isRef {
+			b.typ = ""
+		}
+
+		if !isRef && !b.ctx.structural {
+			b.getDoc(v)
 		}
 	}
 
-	simplify(c, schema)
+	schema := b.finish()
+
+	simplify(b, schema)
 
 	sortSchema(schema)
 
+	b.filled = schema
 	return schema
 }
 
@@ -230,6 +262,8 @@
 	"minLength":        16,
 	"maxLength":        15,
 	"items":            14,
+	"enum":             13,
+	"default":          12,
 }
 
 func (b *builder) resolve(v cue.Value) cue.Value {
@@ -302,8 +336,9 @@
 			switch {
 			case isConcrete(v):
 				b.dispatch(f, v)
-				b.set("enum", []interface{}{b.decode(v)})
-
+				if !b.isNonCore() {
+					b.set("enum", []interface{}{b.decode(v)})
+				}
 			default:
 				if a := appendSplit(nil, cue.OrOp, v); len(a) > 1 {
 					b.disjunction(a, f)
@@ -331,7 +366,9 @@
 			}
 			fallthrough
 		default:
-			b.setFilter("Schema", "default", v)
+			if !b.isNonCore() {
+				b.setFilter("Schema", "default", v)
+			}
 		}
 	}
 	return isRef
@@ -418,34 +455,64 @@
 		if len(disjuncts) == 1 {
 			b.value(disjuncts[0], f)
 		}
-		if len(enums) > 0 {
+		if len(enums) > 0 && !b.isNonCore() {
 			b.set("enum", enums)
 		}
 		if nullable {
-			b.set("nullable", true)
+			b.setSingle("nullable", true, true) // allowed in Structural
 		}
 		return
 	}
 
-	b.addConjunct(func(b *builder) {
-		anyOf := []*oaSchema{}
-		if len(enums) > 0 {
-			anyOf = append(anyOf, b.kv("enum", enums))
-		}
+	anyOf := []*oaSchema{}
+	if len(enums) > 0 {
+		anyOf = append(anyOf, b.kv("enum", enums))
+	}
 
-		for _, v := range disjuncts {
-			c := newOASBuilder(b)
-			c.value(v, f)
-			anyOf = append(anyOf, c.finish())
+	hasEmpty := false
+	for _, v := range disjuncts {
+		c := newOASBuilder(b)
+		c.value(v, f)
+		t := c.finish()
+		if len(t.kvs) == 0 {
+			hasEmpty = true
 		}
+		anyOf = append(anyOf, t)
+	}
 
-		// TODO: analyze CUE structs to figure out if it should be oneOf or
-		// anyOf. As the source is protobuf for now, it is always oneOf.
+	// If any of the types was "any", a oneOf may be discarded.
+	if !hasEmpty {
 		b.set("oneOf", anyOf)
-		if nullable {
-			b.set("nullable", true)
-		}
-	})
+	}
+
+	// TODO: analyze CUE structs to figure out if it should be oneOf or
+	// anyOf. As the source is protobuf for now, it is always oneOf.
+	if nullable {
+		b.setSingle("nullable", true, true)
+	}
+}
+
+func (b *builder) setValueType(v cue.Value) {
+	if b.core != nil {
+		return
+	}
+
+	switch v.IncompleteKind() &^ cue.BottomKind {
+	case cue.BoolKind:
+		b.typ = "boolean"
+	case cue.FloatKind, cue.NumberKind:
+		b.typ = "number"
+	case cue.IntKind:
+		b.typ = "integer"
+	case cue.BytesKind:
+		b.typ = "string"
+	case cue.StringKind:
+		b.typ = "string"
+	case cue.StructKind:
+		b.typ = "object"
+	case cue.ListKind:
+		b.typ = "array"
+	}
 }
 
 func (b *builder) dispatch(f typeFunc, v cue.Value) {
@@ -458,7 +525,7 @@
 	case cue.NullKind:
 		// TODO: for JSON schema we would set the type here. For OpenAPI,
 		// it must be nullable.
-		b.set("nullable", true)
+		b.setSingle("nullable", true, true)
 
 	case cue.BoolKind:
 		b.setType("boolean", "")
@@ -554,16 +621,36 @@
 		b.setFilter("Schema", "required", required)
 	}
 
-	properties := &OrderedMap{}
-	for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
-		properties.Set(i.Label(), b.schema(i.Label(), i.Value()))
+	var properties *OrderedMap
+	if b.singleFields != nil {
+		properties = b.singleFields.getMap("properties")
 	}
-	if len(properties.kvs) > 0 {
-		b.set("properties", properties)
+	hasProps := properties != nil
+	if !hasProps {
+		properties = &OrderedMap{}
 	}
 
-	if t, ok := v.Elem(); ok {
-		b.setFilter("Schema", "additionalProperties", b.schema("*", t))
+	for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
+		label := i.Label()
+		var core *builder
+		if b.core != nil {
+			core = b.core.properties[label]
+		}
+		schema := b.schema(core, label, i.Value())
+		if !b.isNonCore() || len(schema.kvs) > 0 {
+			properties.Set(label, schema)
+		}
+	}
+
+	if !hasProps && len(properties.kvs) > 0 {
+		b.setSingle("properties", properties, false)
+	}
+
+	if t, ok := v.Elem(); ok && (b.core == nil || b.core.items == nil) {
+		schema := b.schema(nil, "*", t)
+		if len(schema.kvs) > 0 {
+			b.setSingle("additionalProperties", schema, true) // Not allowed in structural.
+		}
 	}
 
 	// TODO: maxProperties, minProperties: can be done once we allow cap to
@@ -635,7 +722,7 @@
 	items := []*oaSchema{}
 	count := 0
 	for i, _ := v.List(); i.Next(); count++ {
-		items = append(items, b.schema(strconv.Itoa(count), i.Value()))
+		items = append(items, b.schema(nil, strconv.Itoa(count), i.Value()))
 	}
 	if len(items) > 0 {
 		// TODO: per-item schema are not allowed in OpenAPI, only in JSON Schema.
@@ -661,11 +748,15 @@
 
 	if !hasMax || int64(len(items)) < maxLength {
 		if typ, ok := v.Elem(); ok {
-			t := b.schema("*", typ)
+			var core *builder
+			if b.core != nil {
+				core = b.core.items
+			}
+			t := b.schema(core, "*", typ)
 			if len(items) > 0 {
-				b.setFilter("Schema", "additionalItems", t)
-			} else {
-				b.set("items", t)
+				b.setFilter("Schema", "additionalItems", t) // Not allowed in structural.
+			} else if !b.isNonCore() || len(t.kvs) > 0 {
+				b.setSingle("items", t, true)
 			}
 		}
 	}
@@ -855,12 +946,21 @@
 }
 
 type builder struct {
-	ctx     *buildContext
-	typ     string
-	format  string
-	current *oaSchema
-	allOf   []*oaSchema
-	enums   []interface{}
+	ctx          *buildContext
+	typ          string
+	format       string
+	singleFields *oaSchema
+	current      *oaSchema
+	allOf        []*oaSchema
+
+	// Building structural schema
+	core       *builder
+	kind       cue.Kind
+	filled     *oaSchema
+	values     []cue.Value // in structural mode, all values of not and *Of.
+	keys       []string
+	properties map[string]*builder
+	items      *builder
 }
 
 func newRootBuilder(c *buildContext) *builder {
@@ -868,7 +968,12 @@
 }
 
 func newOASBuilder(parent *builder) *builder {
+	core := parent
+	if parent.core != nil {
+		core = parent.core
+	}
 	b := &builder{
+		core:   core,
 		ctx:    parent.ctx,
 		typ:    parent.typ,
 		format: parent.format,
@@ -876,6 +981,10 @@
 	return b
 }
 
+func (b *builder) isNonCore() bool {
+	return b.core != nil
+}
+
 func (b *builder) setType(t, format string) {
 	if b.typ == "" {
 		b.typ = t
@@ -887,8 +996,14 @@
 
 func setType(t *oaSchema, b *builder) {
 	if b.typ != "" {
-		t.Set("type", b.typ)
-		if b.format != "" {
+		if b.core == nil || (b.core.typ != b.typ && !b.ctx.structural) {
+			if !t.exists("type") {
+				t.Set("type", b.typ)
+			}
+		}
+	}
+	if b.format != "" {
+		if b.core == nil || b.core.format != b.format {
 			t.Set("format", b.format)
 		}
 	}
@@ -902,11 +1017,23 @@
 	b.set(key, v)
 }
 
+// setSingle sets a value of which there should only be one.
+func (b *builder) setSingle(key string, v interface{}, drop bool) {
+	if b.singleFields == nil {
+		b.singleFields = &OrderedMap{}
+	}
+	if b.singleFields.exists(key) {
+		if !drop {
+			b.failf(cue.Value{}, "more than one value added for key %q", key)
+		}
+	}
+	b.singleFields.Set(key, v)
+}
+
 func (b *builder) set(key string, v interface{}) {
 	if b.current == nil {
 		b.current = &OrderedMap{}
 		b.allOf = append(b.allOf, b.current)
-		setType(b.current, b)
 	} else if b.current.exists(key) {
 		b.current = &OrderedMap{}
 		b.allOf = append(b.allOf, b.current)
@@ -916,7 +1043,6 @@
 
 func (b *builder) kv(key string, value interface{}) *oaSchema {
 	constraint := &OrderedMap{}
-	setType(constraint, b)
 	constraint.Set(key, value)
 	return constraint
 }
@@ -927,24 +1053,28 @@
 	b.add(not)
 }
 
-func (b *builder) finish() *oaSchema {
+func (b *builder) finish() (t *oaSchema) {
+	if b.filled != nil {
+		return b.filled
+	}
 	switch len(b.allOf) {
 	case 0:
-		t := &OrderedMap{}
-		if b.typ != "" {
-			setType(t, b)
-		}
-		return t
+		t = &OrderedMap{}
 
 	case 1:
-		setType(b.allOf[0], b)
-		return b.allOf[0]
+		t = b.allOf[0]
 
 	default:
-		t := &OrderedMap{}
+		t = &OrderedMap{}
 		t.Set("allOf", b.allOf)
-		return t
 	}
+	if b.singleFields != nil {
+		b.singleFields.kvs = append(b.singleFields.kvs, t.kvs...)
+		t = b.singleFields
+	}
+	setType(t, b)
+	sortSchema(t)
+	return t
 }
 
 func (b *builder) add(t *oaSchema) {
diff --git a/encoding/openapi/crd.go b/encoding/openapi/crd.go
new file mode 100644
index 0000000..28b935f
--- /dev/null
+++ b/encoding/openapi/crd.go
@@ -0,0 +1,167 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package openapi
+
+// This file contains functionality for structural schema, a subset of OpenAPI
+// used for CRDs.
+//
+// See https://kubernetes.io/blog/2019/06/20/crd-structural-schema/ for details.
+//
+// Insofar definitions are compatible, openapi normalizes to structural whenever
+// possible.
+//
+// A core structural schema is only made out of the following fields:
+//
+// - properties
+// - items
+// - additionalProperties
+// - type
+// - nullable
+// - title
+// - descriptions.
+//
+// Where the types must be defined for all fields.
+//
+// In addition, the value validations constraints may be used as defined in
+// OpenAPI, with the restriction that
+//  - within the logical constraints anyOf, allOf, oneOf, and not
+//    additionalProperties, type, nullable, title, and description may not be used.
+//  - all mentioned fields must be defined in the core schema.
+//
+// It appears that CRDs do not allow references.
+//
+
+import (
+	"cuelang.org/go/cue"
+)
+
+// newCoreBuilder returns a builder that represents a structural schema.
+func newCoreBuilder(c *buildContext) *builder {
+	b := newRootBuilder(c)
+	b.properties = map[string]*builder{}
+	return b
+}
+
+// coreSchema creates the core part of a structural OpenAPI.
+func (b *builder) coreSchema(name string) *oaSchema {
+	oldPath := b.ctx.path
+	b.ctx.path = append(b.ctx.path, name)
+	defer func() { b.ctx.path = oldPath }()
+
+	switch b.kind {
+	case cue.ListKind:
+		if b.items != nil {
+			b.setType("array", "")
+			schema := b.items.coreSchema("*")
+			b.setSingle("items", schema, false)
+		}
+
+	case cue.StructKind:
+		p := &OrderedMap{}
+		for _, k := range b.keys {
+			sub := b.properties[k]
+			p.Set(k, sub.coreSchema(k))
+		}
+		if len(p.kvs) > 0 || b.items != nil {
+			b.setType("object", "")
+		}
+		if len(p.kvs) > 0 {
+			b.setSingle("properties", p, false)
+		}
+		// TODO: in Structural schema only one of these is allowed.
+		if b.items != nil {
+			schema := b.items.coreSchema("*")
+			b.setSingle("additionalProperties", schema, false)
+		}
+	}
+
+	// If there was only a single value associated with this node, we can
+	// safely assume there were no disjunctions etc. In structural mode this
+	// is the only chance we get to set certain properties.
+	if len(b.values) == 1 {
+		return b.fillSchema(b.values[0])
+	}
+
+	// TODO: do type analysis if we have multiple values and piece out more
+	// information that applies to all possible instances.
+
+	return b.finish()
+}
+
+// buildCore collects the CUE values for the structural OpenAPI tree.
+// To this extent, all fields of both conjunctions and disjunctions are
+// collected in a single properties map.
+func (b *builder) buildCore(v cue.Value) {
+	if !b.ctx.expandRefs {
+		_, r := v.Reference()
+		if len(r) > 0 {
+			return
+		}
+	}
+	b.getDoc(v)
+	format := extractFormat(v)
+	if format != "" {
+		b.format = format
+	} else {
+		v = v.Eval()
+		b.kind = v.IncompleteKind() &^ cue.BottomKind
+
+		switch b.kind {
+		case cue.StructKind:
+			if typ, ok := v.Elem(); ok {
+				if b.items == nil {
+					b.items = newCoreBuilder(b.ctx)
+				}
+				b.items.buildCore(typ)
+			}
+			b.buildCoreStruct(v)
+
+		case cue.ListKind:
+			if typ, ok := v.Elem(); ok {
+				if b.items == nil {
+					b.items = newCoreBuilder(b.ctx)
+				}
+				b.items.buildCore(typ)
+			}
+		}
+	}
+
+	for _, bv := range b.values {
+		if bv.Equals(v) {
+			return
+		}
+	}
+	b.values = append(b.values, v)
+}
+
+func (b *builder) buildCoreStruct(v cue.Value) {
+	op, args := v.Expr()
+	switch op {
+	case cue.OrOp, cue.AndOp:
+		for _, v := range args {
+			b.buildCore(v)
+		}
+	}
+	for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
+		label := i.Label()
+		sub, ok := b.properties[label]
+		if !ok {
+			sub = newCoreBuilder(b.ctx)
+			b.properties[label] = sub
+			b.keys = append(b.keys, label)
+		}
+		sub.buildCore(i.Value())
+	}
+}
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index 1459527..ea634ac 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -39,6 +39,10 @@
 		in, out string
 		config  *Config
 	}{{
+		"structural.cue",
+		"structural.json",
+		resolveRefs,
+	}, {
 		"simple.cue",
 		"simple.json",
 		resolveRefs,
@@ -138,3 +142,27 @@
 		})
 	}
 }
+
+// This is for debugging purposes. Do not remove.
+func TestX(t *testing.T) {
+	t.Skip()
+
+	var r cue.Runtime
+	inst, err := r.Compile("test", `
+	AnyField: "any value"
+	`)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	b, err := Gen(inst, &Config{
+		ExpandReferences: true,
+	})
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var out = &bytes.Buffer{}
+	_ = json.Indent(out, b, "", "   ")
+	t.Error(out.String())
+}
diff --git a/encoding/openapi/testdata/array.json b/encoding/openapi/testdata/array.json
index 570d659..5a9136a 100644
--- a/encoding/openapi/testdata/array.json
+++ b/encoding/openapi/testdata/array.json
@@ -9,12 +9,13 @@
                "bar": {
                   "type": "array",
                   "items": {
-                     "default": "1",
+                     "type": "string",
                      "enum": [
                         "1",
                         "2",
                         "3"
-                     ]
+                     ],
+                     "default": "1"
                   }
                },
                "foo": {
@@ -28,12 +29,13 @@
                         "e": {
                            "type": "array",
                            "items": {
-                              "default": "1",
+                              "type": "string",
                               "enum": [
                                  "1",
                                  "2",
                                  "3"
-                              ]
+                              ],
+                              "default": "1"
                            }
                         }
                      }
@@ -52,12 +54,13 @@
          },
          "MyEnum": {
             "description": "MyEnum",
-            "default": "1",
+            "type": "string",
             "enum": [
                "1",
                "2",
                "3"
-            ]
+            ],
+            "default": "1"
          },
          "MyStruct": {
             "description": "MyStruct",
@@ -69,12 +72,13 @@
                "e": {
                   "type": "array",
                   "items": {
-                     "default": "1",
+                     "type": "string",
                      "enum": [
                         "1",
                         "2",
                         "3"
-                     ]
+                     ],
+                     "default": "1"
                   }
                }
             }
diff --git a/encoding/openapi/testdata/nums.json b/encoding/openapi/testdata/nums.json
index fb6efba..69874fe 100644
--- a/encoding/openapi/testdata/nums.json
+++ b/encoding/openapi/testdata/nums.json
@@ -10,14 +10,11 @@
          "neq": {
             "type": "number",
             "not": {
-               "type": "number",
                "allOff": [
                   {
-                     "type": "number",
                      "minimum": 4
                   },
                   {
-                     "type": "number",
                      "maximum": 4
                   }
                ]
diff --git a/encoding/openapi/testdata/oneof-funcs.json b/encoding/openapi/testdata/oneof-funcs.json
index dd0755e..157b53c 100644
--- a/encoding/openapi/testdata/oneof-funcs.json
+++ b/encoding/openapi/testdata/oneof-funcs.json
@@ -8,9 +8,9 @@
       "schemas": {
          "MYSTRING": {
             "description": "Randomly picked description from a set of size one.",
+            "type": "object",
             "oneOf": [
                {
-                  "type": "object",
                   "required": [
                      "exact"
                   ],
@@ -23,7 +23,6 @@
                   }
                },
                {
-                  "type": "object",
                   "required": [
                      "regex"
                   ],
diff --git a/encoding/openapi/testdata/oneof-resolve.json b/encoding/openapi/testdata/oneof-resolve.json
index 217dc9f..629e0dd 100644
--- a/encoding/openapi/testdata/oneof-resolve.json
+++ b/encoding/openapi/testdata/oneof-resolve.json
@@ -7,30 +7,27 @@
    "components": {
       "schemas": {
          "MyString": {
+            "type": "object",
+            "properties": {
+               "exact": {
+                  "type": "string",
+                  "format": "string"
+               },
+               "regex": {
+                  "type": "string",
+                  "format": "string"
+               }
+            },
             "oneOf": [
                {
-                  "type": "object",
                   "required": [
                      "exact"
-                  ],
-                  "properties": {
-                     "exact": {
-                        "type": "string",
-                        "format": "string"
-                     }
-                  }
+                  ]
                },
                {
-                  "type": "object",
                   "required": [
                      "regex"
-                  ],
-                  "properties": {
-                     "regex": {
-                        "type": "string",
-                        "format": "string"
-                     }
-                  }
+                  ]
                }
             ]
          },
@@ -46,60 +43,54 @@
             ],
             "properties": {
                "include": {
+                  "type": "object",
+                  "properties": {
+                     "exact": {
+                        "type": "string",
+                        "format": "string"
+                     },
+                     "regex": {
+                        "type": "string",
+                        "format": "string"
+                     }
+                  },
                   "oneOf": [
                      {
-                        "type": "object",
                         "required": [
                            "exact"
-                        ],
-                        "properties": {
-                           "exact": {
-                              "type": "string",
-                              "format": "string"
-                           }
-                        }
+                        ]
                      },
                      {
-                        "type": "object",
                         "required": [
                            "regex"
-                        ],
-                        "properties": {
-                           "regex": {
-                              "type": "string",
-                              "format": "string"
-                           }
-                        }
+                        ]
                      }
                   ]
                },
                "exclude": {
                   "type": "array",
                   "items": {
+                     "type": "object",
+                     "properties": {
+                        "exact": {
+                           "type": "string",
+                           "format": "string"
+                        },
+                        "regex": {
+                           "type": "string",
+                           "format": "string"
+                        }
+                     },
                      "oneOf": [
                         {
-                           "type": "object",
                            "required": [
                               "exact"
-                           ],
-                           "properties": {
-                              "exact": {
-                                 "type": "string",
-                                 "format": "string"
-                              }
-                           }
+                           ]
                         },
                         {
-                           "type": "object",
                            "required": [
                               "regex"
-                           ],
-                           "properties": {
-                              "regex": {
-                                 "type": "string",
-                                 "format": "string"
-                              }
-                           }
+                           ]
                         }
                      ]
                   }
diff --git a/encoding/openapi/testdata/oneof.json b/encoding/openapi/testdata/oneof.json
index 2781b7b..b2c0c97 100644
--- a/encoding/openapi/testdata/oneof.json
+++ b/encoding/openapi/testdata/oneof.json
@@ -4,9 +4,9 @@
    "components": {
       "schemas": {
          "MyString": {
+            "type": "object",
             "oneOf": [
                {
-                  "type": "object",
                   "required": [
                      "exact"
                   ],
@@ -18,7 +18,6 @@
                   }
                },
                {
-                  "type": "object",
                   "required": [
                      "regex"
                   ],
diff --git a/encoding/openapi/testdata/openapi-norefs.json b/encoding/openapi/testdata/openapi-norefs.json
index 658290e..0c7e922 100644
--- a/encoding/openapi/testdata/openapi-norefs.json
+++ b/encoding/openapi/testdata/openapi-norefs.json
@@ -8,76 +8,63 @@
       "schemas": {
          "MyMessage": {
             "description": "MyMessage is my message.",
-            "allOf": [
-               {
+            "type": "object",
+            "required": [
+               "foo",
+               "bar"
+            ],
+            "properties": {
+               "port": {
                   "type": "object",
                   "required": [
-                     "foo",
-                     "bar"
+                     "port",
+                     "obj"
                   ],
                   "properties": {
                      "port": {
-                        "type": "object",
-                        "required": [
-                           "port",
-                           "obj"
-                        ],
-                        "properties": {
-                           "port": {
-                              "type": "integer"
-                           },
-                           "obj": {
-                              "type": "array",
-                              "items": {
-                                 "type": "integer"
-                              }
-                           }
-                        }
+                        "type": "integer"
                      },
-                     "foo": {
-                        "type": "number",
-                        "exclusiveMinimum": 10,
-                        "exclusiveMaximum": 1000
-                     },
-                     "bar": {
+                     "obj": {
                         "type": "array",
                         "items": {
-                           "type": "string",
-                           "format": "string"
+                           "type": "integer"
                         }
                      }
                   }
                },
+               "foo": {
+                  "type": "number",
+                  "exclusiveMinimum": 10,
+                  "exclusiveMaximum": 1000
+               },
+               "bar": {
+                  "type": "array",
+                  "items": {
+                     "type": "string",
+                     "format": "string"
+                  }
+               },
+               "a": {
+                  "description": "Field a.",
+                  "type": "integer",
+                  "enum": [
+                     1
+                  ]
+               },
+               "b": {
+                  "type": "string",
+                  "format": "string"
+               }
+            },
+            "oneOf": [
                {
-                  "type": "object",
-                  "oneOf": [
-                     {
-                        "type": "object",
-                        "required": [
-                           "a"
-                        ],
-                        "properties": {
-                           "a": {
-                              "description": "Field a.",
-                              "type": "integer",
-                              "enum": [
-                                 1
-                              ]
-                           }
-                        }
-                     },
-                     {
-                        "type": "object",
-                        "required": [
-                           "b"
-                        ],
-                        "properties": {
-                           "b": {
-                              "type": "string",
-                              "format": "string"
-                           }
-                        }
-                     }
+                  "required": [
+                     "a"
+                  ]
+               },
+               {
+                  "required": [
+                     "b"
                   ]
                }
             ]
@@ -105,150 +92,122 @@
             "format": "int32"
          },
          "YourMessage": {
+            "type": "object",
+            "properties": {
+               "a": {
+                  "type": "string",
+                  "format": "string"
+               },
+               "b": {
+                  "format": "string"
+               }
+            },
             "oneOf": [
                {
-                  "type": "object",
                   "required": [
                      "b"
-                  ],
-                  "properties": {
-                     "a": {
-                        "type": "string",
-                        "format": "string"
-                     },
-                     "b": {
-                        "type": "string",
-                        "format": "string"
-                     }
-                  }
+                  ]
                },
                {
-                  "type": "object",
                   "required": [
                      "b"
-                  ],
-                  "properties": {
-                     "a": {
-                        "type": "string",
-                        "format": "string"
-                     },
-                     "b": {
-                        "type": "number"
-                     }
-                  }
+                  ]
                }
             ]
          },
          "YourMessage2": {
+            "type": "object",
+            "properties": {
+               "a": {
+                  "type": "number"
+               },
+               "c": {
+                  "type": "number"
+               },
+               "e": {
+                  "type": "number"
+               },
+               "f": {
+                  "type": "number"
+               },
+               "d": {
+                  "type": "number"
+               },
+               "b": {
+                  "type": "number"
+               }
+            },
             "allOf": [
                {
                   "oneOf": [
                      {
-                        "type": "object",
                         "required": [
                            "a"
-                        ],
-                        "properties": {
-                           "a": {
-                              "type": "number"
-                           }
-                        }
+                        ]
                      },
                      {
-                        "type": "object",
                         "required": [
                            "b"
-                        ],
-                        "properties": {
-                           "b": {
-                              "type": "number"
-                           }
-                        }
+                        ]
                      }
                   ]
                },
                {
                   "oneOf": [
                      {
-                        "type": "object",
                         "required": [
                            "c"
-                        ],
-                        "properties": {
-                           "c": {
-                              "type": "number"
-                           }
-                        }
+                        ]
                      },
                      {
-                        "type": "object",
                         "required": [
                            "d"
-                        ],
-                        "properties": {
-                           "d": {
-                              "type": "number"
-                           }
-                        }
+                        ]
                      }
                   ]
                },
                {
                   "oneOf": [
                      {
-                        "type": "object",
                         "required": [
                            "e"
-                        ],
-                        "properties": {
-                           "e": {
-                              "type": "number"
-                           }
-                        }
+                        ]
                      },
                      {
-                        "type": "object",
                         "required": [
                            "f"
-                        ],
-                        "properties": {
-                           "f": {
-                              "type": "number"
-                           }
-                        }
+                        ]
                      }
                   ]
                }
             ]
          },
          "Msg2": {
+            "type": "object",
+            "properties": {
+               "b": {
+                  "type": "number"
+               },
+               "a": {
+                  "type": "string",
+                  "format": "string"
+               }
+            },
             "oneOf": [
                {
-                  "type": "object",
                   "required": [
                      "b"
-                  ],
-                  "properties": {
-                     "b": {
-                        "type": "number"
-                     }
-                  }
+                  ]
                },
                {
-                  "type": "object",
                   "required": [
                      "a"
-                  ],
-                  "properties": {
-                     "a": {
-                        "type": "string",
-                        "format": "string"
-                     }
-                  }
+                  ]
                }
             ]
          },
          "Enum": {
+            "type": "string",
             "enum": [
                "foo",
                "bar",
@@ -267,47 +226,30 @@
             ]
          },
          "DefaultStruct": {
-            "allOf": [
+            "type": "object",
+            "properties": {
+               "port": {},
+               "obj": {
+                  "type": "array",
+                  "items": {
+                     "type": "integer"
+                  }
+               }
+            },
+            "default": {
+               "port": 1
+            },
+            "oneOf": [
                {
-                  "oneOf": [
-                     {
-                        "type": "object",
-                        "required": [
-                           "port",
-                           "obj"
-                        ],
-                        "properties": {
-                           "port": {
-                              "type": "integer"
-                           },
-                           "obj": {
-                              "type": "array",
-                              "items": {
-                                 "type": "integer"
-                              }
-                           }
-                        }
-                     },
-                     {
-                        "type": "object",
-                        "required": [
-                           "port"
-                        ],
-                        "properties": {
-                           "port": {
-                              "type": "integer",
-                              "enum": [
-                                 1
-                              ]
-                           }
-                        }
-                     }
+                  "required": [
+                     "port",
+                     "obj"
                   ]
                },
                {
-                  "default": {
-                     "port": 1
-                  }
+                  "required": [
+                     "port"
+                  ]
                }
             ]
          }
diff --git a/encoding/openapi/testdata/openapi.json b/encoding/openapi/testdata/openapi.json
index c60e19e..a8b888a 100644
--- a/encoding/openapi/testdata/openapi.json
+++ b/encoding/openapi/testdata/openapi.json
@@ -5,70 +5,61 @@
       "schemas": {
          "MyMessage": {
             "description": "MyMessage is my message.",
-            "allOf": [
-               {
+            "type": "object",
+            "required": [
+               "foo",
+               "bar"
+            ],
+            "properties": {
+               "port": {
                   "type": "object",
+                  "$ref": "#/components/schemas/Port"
+               },
+               "foo": {
+                  "type": "number",
+                  "allOf": [
+                     {
+                        "$ref": "#/components/schemas/Int32"
+                     },
+                     {
+                        "exclusiveMinimum": 10,
+                        "exclusiveMaximum": 1000
+                     }
+                  ]
+               },
+               "bar": {
+                  "type": "array",
+                  "items": {
+                     "type": "string",
+                     "format": "string"
+                  }
+               }
+            },
+            "oneOf": [
+               {
                   "required": [
-                     "foo",
-                     "bar"
+                     "a"
                   ],
                   "properties": {
-                     "port": {
-                        "type": "object",
-                        "$ref": "#/components/schemas/Port"
-                     },
-                     "foo": {
-                        "allOf": [
-                           {
-                              "$ref": "#/components/schemas/Int32"
-                           },
-                           {
-                              "type": "number",
-                              "exclusiveMinimum": 10,
-                              "exclusiveMaximum": 1000
-                           }
+                     "a": {
+                        "description": "Field a.",
+                        "type": "integer",
+                        "enum": [
+                           1
                         ]
-                     },
-                     "bar": {
-                        "type": "array",
-                        "items": {
-                           "type": "string",
-                           "format": "string"
-                        }
                      }
                   }
                },
                {
-                  "type": "object",
-                  "oneOf": [
-                     {
-                        "type": "object",
-                        "required": [
-                           "a"
-                        ],
-                        "properties": {
-                           "a": {
-                              "description": "Field a.",
-                              "type": "integer",
-                              "enum": [
-                                 1
-                              ]
-                           }
-                        }
-                     },
-                     {
-                        "type": "object",
-                        "required": [
-                           "b"
-                        ],
-                        "properties": {
-                           "b": {
-                              "type": "string",
-                              "format": "string"
-                           }
-                        }
+                  "required": [
+                     "b"
+                  ],
+                  "properties": {
+                     "b": {
+                        "type": "string",
+                        "format": "string"
                      }
-                  ]
+                  }
                }
             ]
          },
@@ -95,9 +86,9 @@
             "format": "int32"
          },
          "YourMessage": {
+            "type": "object",
             "oneOf": [
                {
-                  "type": "object",
                   "required": [
                      "b"
                   ],
@@ -113,7 +104,6 @@
                   }
                },
                {
-                  "type": "object",
                   "required": [
                      "b"
                   ],
@@ -130,11 +120,11 @@
             ]
          },
          "YourMessage2": {
+            "type": "object",
             "allOf": [
                {
                   "oneOf": [
                      {
-                        "type": "object",
                         "required": [
                            "a"
                         ],
@@ -145,7 +135,6 @@
                         }
                      },
                      {
-                        "type": "object",
                         "required": [
                            "b"
                         ],
@@ -160,7 +149,6 @@
                {
                   "oneOf": [
                      {
-                        "type": "object",
                         "required": [
                            "c"
                         ],
@@ -171,7 +159,6 @@
                         }
                      },
                      {
-                        "type": "object",
                         "required": [
                            "d"
                         ],
@@ -186,7 +173,6 @@
                {
                   "oneOf": [
                      {
-                        "type": "object",
                         "required": [
                            "e"
                         ],
@@ -197,7 +183,6 @@
                         }
                      },
                      {
-                        "type": "object",
                         "required": [
                            "f"
                         ],
@@ -212,9 +197,9 @@
             ]
          },
          "Msg2": {
+            "type": "object",
             "oneOf": [
                {
-                  "type": "object",
                   "required": [
                      "b"
                   ],
@@ -225,7 +210,6 @@
                   }
                },
                {
-                  "type": "object",
                   "required": [
                      "a"
                   ],
@@ -239,6 +223,7 @@
             ]
          },
          "Enum": {
+            "type": "string",
             "enum": [
                "foo",
                "bar",
@@ -257,31 +242,25 @@
             ]
          },
          "DefaultStruct": {
-            "allOf": [
+            "type": "object",
+            "default": {
+               "port": 1
+            },
+            "oneOf": [
                {
-                  "oneOf": [
-                     {
-                        "$ref": "#/components/schemas/Port"
-                     },
-                     {
-                        "type": "object",
-                        "required": [
-                           "port"
-                        ],
-                        "properties": {
-                           "port": {
-                              "type": "integer",
-                              "enum": [
-                                 1
-                              ]
-                           }
-                        }
-                     }
-                  ]
+                  "$ref": "#/components/schemas/Port"
                },
                {
-                  "default": {
-                     "port": 1
+                  "required": [
+                     "port"
+                  ],
+                  "properties": {
+                     "port": {
+                        "type": "integer",
+                        "enum": [
+                           1
+                        ]
+                     }
                   }
                }
             ]
diff --git a/encoding/openapi/testdata/strings.json b/encoding/openapi/testdata/strings.json
index d62bfff..4b8123b 100644
--- a/encoding/openapi/testdata/strings.json
+++ b/encoding/openapi/testdata/strings.json
@@ -23,7 +23,6 @@
                "myAntiPattern": {
                   "type": "string",
                   "not": {
-                     "type": "string",
                      "pattern": "foo.*bar"
                   }
                }
diff --git a/encoding/openapi/testdata/structural.cue b/encoding/openapi/testdata/structural.cue
new file mode 100644
index 0000000..f1bb1c6
--- /dev/null
+++ b/encoding/openapi/testdata/structural.cue
@@ -0,0 +1,44 @@
+import "time"
+
+Attributes: {
+	//  A map of attribute name to its value.
+	attributes: {
+		<_>: AttrValue
+	}
+}
+
+//  The attribute value.
+AttrValue: {}
+
+AttrValue: {
+	//  Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI
+	stringValue: string @protobuf(2,name=string_value)
+} | {
+	//  Used for values of type INT64
+	int64Value: int64 @protobuf(3,name=int64_value)
+} | {
+	//  Used for values of type DOUBLE
+	doubleValue: float64 @protobuf(4,type=double,name=double_value)
+} | {
+	//  Used for values of type BOOL
+	boolValue: bool @protobuf(5,name=bool_value)
+} | {
+	//  Used for values of type BYTES
+	bytesValue: bytes @protobuf(6,name=bytes_value)
+} | {
+	//  Used for values of type TIMESTAMP
+	timestampValue: time.Time @protobuf(7,type=google.protobuf.Timestamp,name=timestamp_value)
+} | {
+	//  Used for values of type DURATION
+	durationValue: time.Duration @protobuf(8,type=google.protobuf.Duration,name=duration_value)
+} | {
+	//  Used for values of type STRING_MAP
+	stringMapValue: Attributes_StringMap @protobuf(9,type=StringMap,name=string_map_value)
+}
+
+Attributes_StringMap: {
+	//  Holds a set of name/value pairs.
+	entries: {
+		<_>: string
+	} @protobuf(1,type=map<string,string>)
+}
diff --git a/encoding/openapi/testdata/structural.json b/encoding/openapi/testdata/structural.json
new file mode 100644
index 0000000..b1beee2
--- /dev/null
+++ b/encoding/openapi/testdata/structural.json
@@ -0,0 +1,234 @@
+{
+   "openapi": "3.0.0",
+   "info": {
+      "title": "test",
+      "version": "v1"
+   },
+   "components": {
+      "schemas": {
+         "Attributes": {
+            "type": "object",
+            "required": [
+               "attributes"
+            ],
+            "properties": {
+               "attributes": {
+                  "description": "A map of attribute name to its value.",
+                  "type": "object",
+                  "additionalProperties": {
+                     "type": "object",
+                     "properties": {
+                        "stringValue": {
+                           "description": "Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI",
+                           "type": "string",
+                           "format": "string"
+                        },
+                        "int64Value": {
+                           "description": "Used for values of type INT64",
+                           "type": "integer",
+                           "format": "int64"
+                        },
+                        "doubleValue": {
+                           "description": "Used for values of type DOUBLE",
+                           "type": "number",
+                           "format": "double"
+                        },
+                        "boolValue": {
+                           "description": "Used for values of type BOOL",
+                           "type": "boolean"
+                        },
+                        "bytesValue": {
+                           "description": "Used for values of type BYTES",
+                           "type": "string",
+                           "format": "binary"
+                        },
+                        "timestampValue": {
+                           "description": "Used for values of type TIMESTAMP",
+                           "type": "string",
+                           "format": "dateTime"
+                        },
+                        "durationValue": {
+                           "description": "Used for values of type DURATION",
+                           "type": "string"
+                        },
+                        "stringMapValue": {
+                           "description": "Used for values of type STRING_MAP",
+                           "type": "object",
+                           "required": [
+                              "entries"
+                           ],
+                           "properties": {
+                              "entries": {
+                                 "description": "Holds a set of name/value pairs.",
+                                 "type": "object",
+                                 "additionalProperties": {
+                                    "type": "string",
+                                    "format": "string"
+                                 }
+                              }
+                           }
+                        }
+                     },
+                     "oneOf": [
+                        {
+                           "required": [
+                              "stringValue"
+                           ]
+                        },
+                        {
+                           "required": [
+                              "int64Value"
+                           ]
+                        },
+                        {
+                           "required": [
+                              "doubleValue"
+                           ]
+                        },
+                        {
+                           "required": [
+                              "boolValue"
+                           ]
+                        },
+                        {
+                           "required": [
+                              "bytesValue"
+                           ]
+                        },
+                        {
+                           "required": [
+                              "timestampValue"
+                           ]
+                        },
+                        {
+                           "required": [
+                              "durationValue"
+                           ]
+                        },
+                        {
+                           "required": [
+                              "stringMapValue"
+                           ]
+                        }
+                     ]
+                  }
+               }
+            }
+         },
+         "AttrValue": {
+            "description": "The attribute value.",
+            "type": "object",
+            "properties": {
+               "stringValue": {
+                  "description": "Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI",
+                  "type": "string",
+                  "format": "string"
+               },
+               "int64Value": {
+                  "description": "Used for values of type INT64",
+                  "type": "integer",
+                  "format": "int64"
+               },
+               "doubleValue": {
+                  "description": "Used for values of type DOUBLE",
+                  "type": "number",
+                  "format": "double"
+               },
+               "boolValue": {
+                  "description": "Used for values of type BOOL",
+                  "type": "boolean"
+               },
+               "bytesValue": {
+                  "description": "Used for values of type BYTES",
+                  "type": "string",
+                  "format": "binary"
+               },
+               "timestampValue": {
+                  "description": "Used for values of type TIMESTAMP",
+                  "type": "string",
+                  "format": "dateTime"
+               },
+               "durationValue": {
+                  "description": "Used for values of type DURATION",
+                  "type": "string"
+               },
+               "stringMapValue": {
+                  "description": "Used for values of type STRING_MAP",
+                  "type": "object",
+                  "required": [
+                     "entries"
+                  ],
+                  "properties": {
+                     "entries": {
+                        "description": "Holds a set of name/value pairs.",
+                        "type": "object",
+                        "additionalProperties": {
+                           "type": "string",
+                           "format": "string"
+                        }
+                     }
+                  }
+               }
+            },
+            "oneOf": [
+               {
+                  "required": [
+                     "stringValue"
+                  ]
+               },
+               {
+                  "required": [
+                     "int64Value"
+                  ]
+               },
+               {
+                  "required": [
+                     "doubleValue"
+                  ]
+               },
+               {
+                  "required": [
+                     "boolValue"
+                  ]
+               },
+               {
+                  "required": [
+                     "bytesValue"
+                  ]
+               },
+               {
+                  "required": [
+                     "timestampValue"
+                  ]
+               },
+               {
+                  "required": [
+                     "durationValue"
+                  ]
+               },
+               {
+                  "required": [
+                     "stringMapValue"
+                  ]
+               }
+            ]
+         },
+         "Attributes_StringMap": {
+            "type": "object",
+            "required": [
+               "entries"
+            ],
+            "properties": {
+               "entries": {
+                  "description": "Holds a set of name/value pairs.",
+                  "type": "object",
+                  "additionalProperties": {
+                     "type": "string",
+                     "format": "string"
+                  }
+               }
+            }
+         }
+      }
+   }
+}
\ No newline at end of file