pkg/runtime now has a well defined api pluggability model.

pull/6/head
Daniel Smith 2014-09-10 17:28:07 -07:00
parent 71e547124c
commit 52d2c221b8
5 changed files with 534 additions and 90 deletions

55
pkg/runtime/extension.go Normal file
View File

@ -0,0 +1,55 @@
/*
Copyright 2014 Google Inc. All rights reserved.
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 runtime
import (
"gopkg.in/v1/yaml"
)
func (re *RawExtension) UnmarshalJSON(in []byte) error {
re.RawJSON = in
return nil
}
func (re *RawExtension) MarshalJSON() ([]byte, error) {
return re.RawJSON, nil
}
// SetYAML implements the yaml.Setter interface.
func (re *RawExtension) SetYAML(tag string, value interface{}) bool {
if value == nil {
re.RawJSON = []byte("null")
return true
}
// Why does the yaml package send value as a map[interface{}]interface{}?
// It's especially frustrating because encoding/json does the right thing
// by giving a []byte. So here we do the embarrasing thing of re-encode and
// de-encode the right way.
// TODO: Write a version of Decode that uses reflect to turn this value
// into an API object.
b, err := yaml.Marshal(value)
if err != nil {
panic("yaml can't reverse its own object")
}
re.RawJSON = b
return true
}
// GetYAML implements the yaml.Getter interface.
func (re *RawExtension) GetYAML() (tag string, value interface{}) {
return tag, re.RawJSON
}

View File

@ -1,81 +0,0 @@
/*
Copyright 2014 Google Inc. All rights reserved.
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 runtime_test
import (
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
_ "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
func TestEncode(t *testing.T) {
pod := &api.Pod{
Labels: map[string]string{"name": "foo"},
}
obj := runtime.Object(pod)
data, err := runtime.DefaultScheme.Encode(obj)
obj2, err2 := runtime.DefaultScheme.Decode(data)
if err != nil || err2 != nil {
t.Fatalf("Failure: '%v' '%v'", err, err2)
}
if _, ok := obj2.(*api.Pod); !ok {
t.Fatalf("Got wrong type")
}
if !reflect.DeepEqual(obj2, pod) {
t.Errorf("Expected:\n %#v,\n Got:\n %#v", &pod, obj2)
}
}
func TestBadJSONRejection(t *testing.T) {
badJSONMissingKind := []byte(`{ }`)
if _, err := runtime.DefaultScheme.Decode(badJSONMissingKind); err == nil {
t.Errorf("Did not reject despite lack of kind field: %s", badJSONMissingKind)
}
badJSONUnknownType := []byte(`{"kind": "bar"}`)
if _, err1 := runtime.DefaultScheme.Decode(badJSONUnknownType); err1 == nil {
t.Errorf("Did not reject despite use of unknown type: %s", badJSONUnknownType)
}
/*badJSONKindMismatch := []byte(`{"kind": "Pod"}`)
if err2 := DecodeInto(badJSONKindMismatch, &Minion{}); err2 == nil {
t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch)
}*/
}
func TestExtractList(t *testing.T) {
pl := &api.PodList{
Items: []api.Pod{
{JSONBase: api.JSONBase{ID: "1"}},
{JSONBase: api.JSONBase{ID: "2"}},
{JSONBase: api.JSONBase{ID: "3"}},
},
}
list, err := runtime.ExtractList(pl)
if err != nil {
t.Fatalf("Unexpected error %v", err)
}
if e, a := len(list), len(pl.Items); e != a {
t.Fatalf("Expected %v, got %v", e, a)
}
for i := range list {
if e, a := list[i].(*api.Pod).ID, pl.Items[i].ID; e != a {
t.Fatalf("Expected %v, got %v", e, a)
}
}
}

View File

@ -34,6 +34,149 @@ type Scheme struct {
raw *conversion.Scheme
}
var namedSchemes map[string]*Scheme
// GetScheme returns the scheme with the given name, creating it if necessary.
// Important: You may not modify the returned *Scheme except from init() functions.
func GetScheme(schemeName string) *Scheme {
if namedSchemes == nil {
namedSchemes = map[string]*Scheme{}
}
if s, ok := namedSchemes[schemeName]; ok {
return s
}
s := NewScheme("", "")
namedSchemes[schemeName] = s
return s
}
// fromScope gets the input version, desired output version, and desired Scheme
// from a conversion.Scope.
func fromScope(s conversion.Scope) (inVersion, outVersion string, scheme *Scheme) {
scheme = DefaultScheme
inVersion = s.Meta()["srcVersion"].(string)
outVersion = s.Meta()["destVersion"].(string)
// If a scheme tag was provided, use it. Look at the struct tag corresponding
// to version "".
if name := s.SrcTag().Get("scheme"); inVersion == "" && name != "" {
scheme = GetScheme(name)
}
if name := s.DestTag().Get("scheme"); outVersion == "" && name != "" {
scheme = GetScheme(name)
}
return inVersion, outVersion, scheme
}
func init() {
// Set up a generic mapping between RawExtension and EmbeddedObject.
DefaultScheme.AddConversionFuncs(
embeddedObjectToRawExtension,
rawExtensionToEmbeddedObject,
)
}
// emptyPlugin is used to copy the Kind field to and from plugin objects.
type emptyPlugin struct {
PluginBase `json:",inline" yaml:",inline"`
}
// embeddedObjectToRawExtension does the conversion you would expect from the name, using the information
// given in conversion.Scope. It's placed in the DefaultScheme as a ConversionFunc to enable plugins;
// see the comment for RawExtension.
func embeddedObjectToRawExtension(in *EmbeddedObject, out *RawExtension, s conversion.Scope) error {
if in.Object == nil {
out.RawJSON = []byte("null")
return nil
}
// Figure out the type and kind of the output object.
_, outVersion, scheme := fromScope(s)
_, kind, err := scheme.raw.ObjectVersionAndKind(in.Object)
if err != nil {
return err
}
// Manufacture an object of this type and kind.
outObj, err := scheme.New(outVersion, kind)
if err != nil {
return err
}
// Manually do the conversion.
err = s.Convert(in.Object, outObj, 0)
if err != nil {
return err
}
// Copy the kind field into the ouput object.
err = s.Convert(
&emptyPlugin{PluginBase: PluginBase{Kind: kind}},
outObj,
conversion.SourceToDest|conversion.IgnoreMissingFields|conversion.AllowDifferentFieldTypeNames,
)
if err != nil {
return err
}
// Because we provide the correct version, EncodeToVersion will not attempt a conversion.
raw, err := scheme.EncodeToVersion(outObj, outVersion)
if err != nil {
// TODO: if this fails, create an Unknown-- maybe some other
// component will understand it.
return err
}
out.RawJSON = raw
return nil
}
// rawExtensionToEmbeddedObject does the conversion you would expect from the name, using the information
// given in conversion.Scope. It's placed in the DefaultScheme as a ConversionFunc to enable plugins;
// see the comment for RawExtension.
func rawExtensionToEmbeddedObject(in *RawExtension, out *EmbeddedObject, s conversion.Scope) error {
if len(in.RawJSON) == 4 && string(in.RawJSON) == "null" {
out.Object = nil
return nil
}
// Figure out the type and kind of the output object.
inVersion, outVersion, scheme := fromScope(s)
_, kind, err := scheme.raw.DataVersionAndKind(in.RawJSON)
if err != nil {
return err
}
// We have to make this object ourselves because we don't store the version field for
// plugin objects.
inObj, err := scheme.New(inVersion, kind)
if err != nil {
return err
}
err = scheme.DecodeInto(in.RawJSON, inObj)
if err != nil {
return err
}
// Make the desired internal version, and do the conversion.
outObj, err := scheme.New(outVersion, kind)
if err != nil {
return err
}
err = scheme.Convert(inObj, outObj)
if err != nil {
return err
}
// Last step, clear the Kind field; that should always be blank in memory.
err = s.Convert(
&emptyPlugin{PluginBase: PluginBase{Kind: ""}},
outObj,
conversion.SourceToDest|conversion.IgnoreMissingFields|conversion.AllowDifferentFieldTypeNames,
)
if err != nil {
return err
}
out.Object = outObj
return nil
}
// NewScheme creates a new Scheme. A default scheme is provided and accessible
// as the "DefaultScheme" variable.
func NewScheme(internalVersion, externalVersion string) *Scheme {
@ -54,6 +197,13 @@ func (s *Scheme) AddKnownTypes(version string, types ...Object) {
s.raw.AddKnownTypes(version, interfaces...)
}
// AddKnownTypeWithName is like AddKnownTypes, but it lets you specify what this type should
// be encoded as. Useful for testing when you don't want to make multiple packages to define
// your structs.
func (s *Scheme) AddKnownTypeWithName(version, kind string, obj Object) {
s.raw.AddKnownTypeWithName(version, kind, obj)
}
// New returns a new API object of the given version ("" for internal
// representation) and name, or an error if it hasn't been registered.
func (s *Scheme) New(versionName, typeName string) (Object, error) {
@ -153,6 +303,11 @@ func (s *Scheme) Encode(obj Object) (data []byte, err error) {
return s.raw.Encode(obj)
}
// EncodeToVersion is like Encode, but lets you specify the destination version.
func (s *Scheme) EncodeToVersion(obj Object, destVersion string) (data []byte, err error) {
return s.raw.EncodeToVersion(obj, destVersion)
}
// enforcePtr ensures that obj is a pointer of some sort. Returns a reflect.Value of the
// dereferenced pointer, ensuring that it is settable/addressable.
// Returns an error if this is not possible.

257
pkg/runtime/scheme_test.go Normal file
View File

@ -0,0 +1,257 @@
/*
Copyright 2014 Google Inc. All rights reserved.
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 runtime_test
import (
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/conversion"
"github.com/GoogleCloudPlatform/kubernetes/pkg/runtime"
)
type JSONBase struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
}
type InternalSimple struct {
JSONBase `json:",inline" yaml:",inline"`
TestString string `json:"testString" yaml:"testString"`
}
type ExternalSimple struct {
JSONBase `json:",inline" yaml:",inline"`
TestString string `json:"testString" yaml:"testString"`
}
func (*InternalSimple) IsAnAPIObject() {}
func (*ExternalSimple) IsAnAPIObject() {}
func TestScheme(t *testing.T) {
runtime.DefaultScheme.AddKnownTypeWithName("", "Simple", &InternalSimple{})
runtime.DefaultScheme.AddKnownTypeWithName("externalVersion", "Simple", &ExternalSimple{})
internalToExternalCalls := 0
externalToInternalCalls := 0
// Register functions to verify that scope.Meta() gets set correctly.
err := runtime.DefaultScheme.AddConversionFuncs(
func(in *InternalSimple, out *ExternalSimple, scope conversion.Scope) error {
if e, a := "", scope.Meta()["srcVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
if e, a := "externalVersion", scope.Meta()["destVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
scope.Convert(&in.JSONBase, &out.JSONBase, 0)
scope.Convert(&in.TestString, &out.TestString, 0)
internalToExternalCalls++
return nil
},
func(in *ExternalSimple, out *InternalSimple, scope conversion.Scope) error {
if e, a := "externalVersion", scope.Meta()["srcVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
if e, a := "", scope.Meta()["destVersion"].(string); e != a {
t.Errorf("Expected '%v', got '%v'", e, a)
}
scope.Convert(&in.JSONBase, &out.JSONBase, 0)
scope.Convert(&in.TestString, &out.TestString, 0)
externalToInternalCalls++
return nil
},
)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
simple := &InternalSimple{
TestString: "foo",
}
// Test Encode, Decode, and DecodeInto
obj := runtime.Object(simple)
data, err := runtime.DefaultScheme.EncodeToVersion(obj, "externalVersion")
obj2, err2 := runtime.DefaultScheme.Decode(data)
obj3 := &InternalSimple{}
err3 := runtime.DefaultScheme.DecodeInto(data, obj3)
if err != nil || err2 != nil {
t.Fatalf("Failure: '%v' '%v' '%v'", err, err2, err3)
}
if _, ok := obj2.(*InternalSimple); !ok {
t.Fatalf("Got wrong type")
}
if e, a := simple, obj2; !reflect.DeepEqual(e, a) {
t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a)
}
if e, a := simple, obj3; !reflect.DeepEqual(e, a) {
t.Errorf("Expected:\n %#v,\n Got:\n %#v", e, a)
}
// Test Convert
external := &ExternalSimple{}
err = runtime.DefaultScheme.Convert(simple, external)
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if e, a := simple.TestString, external.TestString; e != a {
t.Errorf("Expected %v, got %v", e, a)
}
// Encode and Convert should each have caused an increment.
if e, a := 2, internalToExternalCalls; e != a {
t.Errorf("Expected %v, got %v", e, a)
}
// Decode and DecodeInto should each have caused an increment.
if e, a := 2, externalToInternalCalls; e != a {
t.Errorf("Expected %v, got %v", e, a)
}
}
func TestBadJSONRejection(t *testing.T) {
badJSONMissingKind := []byte(`{ }`)
if _, err := runtime.DefaultScheme.Decode(badJSONMissingKind); err == nil {
t.Errorf("Did not reject despite lack of kind field: %s", badJSONMissingKind)
}
badJSONUnknownType := []byte(`{"kind": "bar"}`)
if _, err1 := runtime.DefaultScheme.Decode(badJSONUnknownType); err1 == nil {
t.Errorf("Did not reject despite use of unknown type: %s", badJSONUnknownType)
}
/*badJSONKindMismatch := []byte(`{"kind": "Pod"}`)
if err2 := DecodeInto(badJSONKindMismatch, &Minion{}); err2 == nil {
t.Errorf("Kind is set but doesn't match the object type: %s", badJSONKindMismatch)
}*/
}
type ExtensionA struct {
runtime.PluginBase `json:",inline" yaml:",inline"`
TestString string `json:"testString" yaml:"testString"`
}
type ExtensionB struct {
runtime.PluginBase `json:",inline" yaml:",inline"`
TestString string `json:"testString" yaml:"testString"`
}
type ExternalExtensionType struct {
JSONBase `json:",inline" yaml:",inline"`
Extension runtime.RawExtension `json:"extension" yaml:"extension"`
}
type InternalExtensionType struct {
JSONBase `json:",inline" yaml:",inline"`
Extension runtime.EmbeddedObject `json:"extension" yaml:"extension" scheme:"testExtension"`
}
type InternalExtensionTypeNoScheme struct {
JSONBase `json:",inline" yaml:",inline"`
Extension runtime.EmbeddedObject `json:"extension" yaml:"extension"`
}
func (*ExtensionA) IsAnAPIObject() {}
func (*ExtensionB) IsAnAPIObject() {}
func (*ExternalExtensionType) IsAnAPIObject() {}
func (*InternalExtensionType) IsAnAPIObject() {}
func (*InternalExtensionTypeNoScheme) IsAnAPIObject() {}
func TestExtensionMappingWithScheme(t *testing.T) {
runtime.DefaultScheme.AddKnownTypeWithName("", "ExtensionType", &InternalExtensionType{})
runtime.GetScheme("testExtension").AddKnownTypeWithName("", "A", &ExtensionA{})
runtime.GetScheme("testExtension").AddKnownTypeWithName("", "B", &ExtensionB{})
runtime.DefaultScheme.AddKnownTypeWithName("testExternal", "ExtensionType", &ExternalExtensionType{})
runtime.GetScheme("testExtension").AddKnownTypeWithName("testExternal", "A", &ExtensionA{})
runtime.GetScheme("testExtension").AddKnownTypeWithName("testExternal", "B", &ExtensionB{})
table := []struct {
obj runtime.Object
encoded string
}{
{
&InternalExtensionType{Extension: runtime.EmbeddedObject{&ExtensionA{TestString: "foo"}}},
`{"kind":"ExtensionType","apiVersion":"testExternal","extension":{"kind":"A","testString":"foo"}}`,
}, {
&InternalExtensionType{Extension: runtime.EmbeddedObject{&ExtensionB{TestString: "bar"}}},
`{"kind":"ExtensionType","apiVersion":"testExternal","extension":{"kind":"B","testString":"bar"}}`,
}, {
&InternalExtensionType{Extension: runtime.EmbeddedObject{nil}},
`{"kind":"ExtensionType","apiVersion":"testExternal","extension":null}`,
},
}
for _, item := range table {
gotEncoded, err := runtime.DefaultScheme.EncodeToVersion(item.obj, "testExternal")
if err != nil {
t.Errorf("unexpected error '%v' (%#v)", err, item.obj)
} else if e, a := item.encoded, string(gotEncoded); e != a {
t.Errorf("expected %v, got %v", e, a)
}
gotDecoded, err := runtime.DefaultScheme.Decode([]byte(item.encoded))
if err != nil {
t.Errorf("unexpected error '%v' (%v)", err, item.encoded)
} else if e, a := item.obj, gotDecoded; !reflect.DeepEqual(e, a) {
var eEx, aEx runtime.Object
if obj, ok := e.(*InternalExtensionType); ok {
eEx = obj.Extension.Object
}
if obj, ok := a.(*InternalExtensionType); ok {
aEx = obj.Extension.Object
}
t.Errorf("expected %#v, got %#v (%#v, %#v)", e, a, eEx, aEx)
}
}
}
func TestExtensionMappingWithoutScheme(t *testing.T) {
runtime.DefaultScheme.AddKnownTypeWithName("", "ExtensionType", &InternalExtensionTypeNoScheme{})
runtime.DefaultScheme.AddKnownTypeWithName("", "ExA", &ExtensionA{})
runtime.DefaultScheme.AddKnownTypeWithName("testExternal", "ExtensionType", &ExternalExtensionType{})
runtime.DefaultScheme.AddKnownTypeWithName("testExternal", "ExA", &ExtensionA{})
table := []struct {
obj runtime.Object
encoded string
}{
{
&InternalExtensionTypeNoScheme{Extension: runtime.EmbeddedObject{&ExtensionA{TestString: "foo"}}},
`{"kind":"ExtensionType","apiVersion":"testExternal","extension":{"kind":"ExA","testString":"foo"}}`,
},
}
for _, item := range table {
gotEncoded, err := runtime.DefaultScheme.EncodeToVersion(item.obj, "testExternal")
if err != nil {
t.Errorf("unexpected error '%v' (%#v)", err, item.obj)
} else if e, a := item.encoded, string(gotEncoded); e != a {
t.Errorf("expected %v, got %v", e, a)
}
gotDecoded, err := runtime.DefaultScheme.Decode([]byte(item.encoded))
if err != nil {
t.Errorf("unexpected error '%v' (%v)", err, item.encoded)
} else if e, a := item.obj, gotDecoded; !reflect.DeepEqual(e, a) {
var eEx, aEx runtime.Object
if obj, ok := e.(*InternalExtensionTypeNoScheme); ok {
eEx = obj.Extension.Object
}
if obj, ok := a.(*InternalExtensionTypeNoScheme); ok {
aEx = obj.Extension.Object
}
t.Errorf("expected %#v, got %#v (%#v, %#v)", e, a, eEx, aEx)
}
}
}

View File

@ -43,6 +43,12 @@ type JSONBase struct {
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"`
}
// PluginBase is like JSONBase, but it's intended for plugin objects that won't ever be encoded
// except while embedded in other objects.
type PluginBase struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
}
// EmbeddedObject has appropriate encoder and decoder functions, such that on the wire, it's
// stored as a []byte, but in memory, the contained object is accessable as an Object
// via the Get() function. Only valid API objects may be stored via EmbeddedObject.
@ -51,18 +57,70 @@ type JSONBase struct {
//
// Note that object assumes that you've registered all of your api types with the api package.
//
// TODO(dbsmith): Stop using runtime.Codec, use the codec appropriate for the conversion (I have a plan).
// EmbeddedObject and RawExtension can be used together to allow for API object extensions:
// see the comment for RawExtension.
type EmbeddedObject struct {
Object
}
// Extension allows api objects with unknown types to be passed-through. This can be used
// to deal with the API objects from a plug-in. Extension objects still have functioning
// JSONBase features-- kind, version, resourceVersion, etc.
// TODO: Not implemented yet
type Extension struct {
JSONBase `yaml:",inline" json:",inline"`
// RawJSON to go here.
// RawExtension is used with EmbeddedObject to do a two-phase encoding of extension objects.
//
// To use this, make a field which has RawExtension as its type in your external, versioned
// struct, and EmbeddedObject in your internal struct. You also need to register your
// various plugin types.
//
// // Internal package:
// type MyAPIObject struct {
// runtime.JSONBase `yaml:",inline" json:",inline"`
// // The "scheme" tag is optional; if absent, runtime.DefaultScheme will be used.
// MyPlugin runtime.EmbeddedObject `scheme:"pluginScheme"`
// }
// type PluginA struct {
// runtime.PluginBase `yaml:",inline" json:",inline"`
// AOption string `yaml:"aOption" json:"aOption"`
// }
//
// // External package:
// type MyAPIObject struct {
// runtime.JSONBase `yaml:",inline" json:",inline"`
// MyPlugin runtime.RawExtension `json:"myPlugin" yaml:"myPlugin"`
// }
// type PluginA struct {
// runtime.PluginBase `yaml:",inline" json:",inline"`
// AOption string `yaml:"aOption" json:"aOption"`
// }
//
// // On the wire, the JSON will look something like this:
// {
// "kind":"MyAPIObject",
// "apiVersion":"v1beta1",
// "myPlugin": {
// "kind":"PluginA",
// "aOption":"foo",
// },
// }
//
// So what happens? Decode first uses json or yaml to unmarshal the serialized data into
// your external MyAPIObject. That causes the raw JSON to be stored, but not unpacked.
// The next step is to copy (using pkg/conversion) into the internal struct. The runtime
// package's DefaultScheme has conversion functions installed which will unpack the
// JSON stored in RawExtension, turning it into the correct object type, and storing it
// in the EmbeddedObject. (TODO: In the case where the object is of an unknown type, a
// runtime.Unknown object will be created and stored.)
type RawExtension struct {
RawJSON []byte
}
func (*Extension) IsAnAPIObject() {}
// Unknown allows api objects with unknown types to be passed-through. This can be used
// to deal with the API objects from a plug-in. Unknown objects still have functioning
// JSONBase features-- kind, version, resourceVersion, etc.
// TODO: Not implemented yet!
type Unknown struct {
JSONBase `yaml:",inline" json:",inline"`
// RawJSON will hold the complete JSON of the object which couldn't be matched
// with a registered type. Most likely, nothing should be done with this
// except for passing it through the system.
RawJSON []byte
}
func (*Unknown) IsAnAPIObject() {}