Generating Go code from JSON Schema documents

Folks familiar with me and my blog will know I'm one of the Core Maintainers for oapi-codegen
, and am a big fan of generating code from schemas (in a "design first" manner).
When I'm not documenting things with OpenAPI, I'll be documenting things with JSON Schema.
Today I was discussing with a colleague how to generate Go code from JSON Schema, and found that I've never blogged about it π Which is of great concern, because I ended up doing this literally a year ago to the day tomorrow, and not having a blog post is most unlike me πΉ So as part of setting it up on a new repo, I thought I'd write it up as a form of blogumentation.
You can see the examples of the code in this example project on GitLab.com.
The schema
I could've created a straightforward spec by hand for this, but thought that actually, it would be more meaningful to use oapi-codegen
's configuration schema.
oapi-codegen
configuration schema (v2.4.1)
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Configuration files for oapi-codegen",
"type": "object",
"additionalProperties": false,
"properties": {
"package": {
"type": "string",
"description": "Go package name to generate the code under"
},
"generate": {
"type": "object",
"additionalProperties": false,
"description": "Generate specifies which supported output formats to generate",
"properties": {
"iris-server": {
"type": "boolean",
"description": "IrisServer specifies whether to generate iris server boilerplate"
},
"chi-server": {
"type": "boolean",
"description": "ChiServer specifies whether to generate chi server boilerplate"
},
"fiber-server": {
"type": "boolean",
"description": "FiberServer specifies whether to generate fiber server boilerplate"
},
"echo-server": {
"type": "boolean",
"description": "EchoServer specifies whether to generate echo server boilerplate"
},
"gin-server": {
"type": "boolean",
"description": "GinServer specifies whether to generate gin server boilerplate"
},
"gorilla-server": {
"type": "boolean",
"description": "GorillaServer specifies whether to generate Gorilla server boilerplate"
},
"std-http-server": {
"type": "boolean",
"description": "StdHTTPServer specifies whether to generate stdlib http server boilerplate"
},
"strict-server": {
"type": "boolean",
"description": "Strict specifies whether to generate strict server wrapper"
},
"client": {
"type": "boolean",
"description": "Client specifies whether to generate client boilerplate"
},
"models": {
"type": "boolean",
"description": "Models specifies whether to generate type definitions"
},
"embedded-spec": {
"type": "boolean",
"description": "EmbeddedSpec indicates whether to embed the swagger spec in the generated code"
}
}
},
"compatibility": {
"type": "object",
"additionalProperties": false,
"description": "",
"properties": {
"old-merge-schemas": {
"type": "boolean",
"description": "In the past, we merged schemas for `allOf` by inlining each schema within the schema list. This approach, though, is incorrect because `allOf` merges at the schema definition level, not at the resulting model level. So, new behavior merges OpenAPI specs but generates different code than we have in the past. Set OldMergeSchemas to true for the old behavior. Please see https://github.com/oapi-codegen/oapi-codegen/issues/531"
},
"old-enum-conflicts": {
"type": "boolean",
"description": "Enum values can generate conflicting typenames, so we've updated the code for enum generation to avoid these conflicts, but it will result in some enum types being renamed in existing code. Set OldEnumConflicts to true to revert to old behavior. Please see: Please see https://github.com/oapi-codegen/oapi-codegen/issues/549"
},
"old-aliasing": {
"type": "boolean",
"description": "It was a mistake to generate a go type definition for every $ref in the OpenAPI schema. New behavior uses type aliases where possible, but this can generate code which breaks existing builds. Set OldAliasing to true for old behavior. Please see https://github.com/oapi-codegen/oapi-codegen/issues/549"
},
"disable-flatten-additional-properties": {
"type": "boolean",
"description": "When an object contains no members, and only an additionalProperties specification, it is flattened to a map"
},
"disable-required-readonly-as-pointer": {
"type": "boolean",
"description": "When an object property is both required and readOnly the go model is generated as a pointer. Set DisableRequiredReadOnlyAsPointer to true to mark them as non pointer. Please see https://github.com/oapi-codegen/oapi-codegen/issues/604"
},
"always-prefix-enum-values": {
"type": "boolean",
"description": "When set to true, always prefix enum values with their type name instead of only when typenames would be conflicting."
},
"apply-chi-middleware-first-to-last": {
"type": "boolean",
"description": "Our generated code for Chi has historically inverted the order in which Chi middleware is applied such that the last invoked middleware ends up executing first in the Chi chain This resolves the behavior such that middlewares are chained in the order they are invoked. Please see https://github.com/oapi-codegen/oapi-codegen/issues/786"
},
"apply-gorilla-middleware-first-to-last": {
"type": "boolean",
"description": "Our generated code for gorilla/mux has historically inverted the order in which gorilla/mux middleware is applied such that the last invoked middleware ends up executing first in the middlewares chain This resolves the behavior such that middlewares are chained in the order they are invoked. Please see https://github.com/oapi-codegen/oapi-codegen/issues/841"
},
"circular-reference-limit": {
"type": "integer",
"description": "DEPRECATED: No longer used.\nCircularReferenceLimit allows controlling the limit for circular reference checking. In some OpenAPI specifications, we have a higher number of circular references than is allowed out-of-the-box, but can be tuned to allow traversing them."
},
"allow-unexported-struct-field-names": {
"type": "boolean",
"description": "AllowUnexportedStructFieldNames makes it possible to output structs that have fields that are unexported.\nThis is expected to be used in conjunction with an extension such as `x-go-name` to override the output name, and `x-oapi-codegen-extra-tags` to not produce JSON tags for `encoding/json`.\nNOTE that this can be confusing to users of your OpenAPI specification, who may see a field present and therefore be expecting to see it in the response, without understanding the nuance of how `oapi-codegen` generates the code."
},
"preserve-original-operation-id-casing-in-embedded-spec": {
"type": "boolean",
"description": "When `oapi-codegen` parses the original OpenAPI specification, it will apply the configured `output-options.name-normalizer` to each operation's `operationId` before that is used to generate code from.\nHowever, this is also applied to the copy of the `operationId`s in the `embedded-spec` generation, which means that the embedded OpenAPI specification is then out-of-sync with the input specificiation.\nTo ensure that the `operationId` in the embedded spec is preserved as-is from the input specification, set this. NOTE that this will not impact generated code.\nNOTE that if you're using `include-operation-ids` or `exclude-operation-ids` you may want to ensure that the `operationId`s used are correct."
}
}
},
"output-options": {
"type": "object",
"additionalProperties": false,
"description": "OutputOptions are used to modify the output code in some way",
"properties": {
"skip-fmt": {
"type": "boolean",
"description": "Whether to skip go imports on the generated code"
},
"skip-prune": {
"type": "boolean",
"description": "Whether to skip pruning unused components on the generated code"
},
"include-tags": {
"type": "array",
"description": "Only include operations that have one of these tags. Ignored when empty.",
"items": {
"type": "string"
}
},
"exclude-tags": {
"type": "array",
"description": "Exclude operations that have one of these tags. Ignored when empty.",
"items": {
"type": "string"
}
},
"include-operation-ids": {
"type": "array",
"description": "Only include operations that have one of these operation-ids. Ignored when empty.",
"items": {
"type": "string"
}
},
"exclude-operation-ids": {
"type": "array",
"description": "Exclude operations that have one of these operation-ids. Ignored when empty.",
"items": {
"type": "string"
}
},
"user-templates": {
"type": "object",
"description": "Override built-in templates from user-provided files",
"additionalProperties": {
"type": "string"
}
},
"exclude-schemas": {
"type": "array",
"description": "Exclude from generation schemas with given names. Ignored when empty.",
"items": {
"type": "string"
}
},
"response-type-suffix": {
"type": "string",
"description": "The suffix used for responses types"
},
"client-type-name": {
"type": "string",
"description": "Override the default generated client type with the value"
},
"initialism-overrides": {
"type": "boolean",
"description": "Whether to use the initialism overrides"
},
"additional-initialisms": {
"type": "array",
"description": "AdditionalInitialisms defines additional initialisms to be used by the code generator. Has no effect unless the `name-normalizer` is set to `ToCamelCaseWithInitialisms`",
"items": {
"type": "string"
}
},
"nullable-type": {
"type": "boolean",
"description": "Whether to generate nullable type for nullable fields"
},
"disable-type-aliases-for-type": {
"type": "array",
"description": "DisableTypeAliasesForType allows defining which OpenAPI `type`s will explicitly not use type aliases",
"items": {
"type": "string",
"enum": [
"array"
]
}
},
"name-normalizer": {
"type": "string",
"description": "NameNormalizer is the method used to normalize Go names and types, for instance converting the text `MyApi` to `MyAPI`. Corresponds with the constants defined for `codegen.NameNormalizerFunction`",
"default": "ToCamelCase",
"enum": [
"ToCamelCase",
"ToCamelCaseWithDigits",
"ToCamelCaseWithInitialisms"
]
},
"overlay": {
"type": "object",
"description": "Overlay defines configuration for the OpenAPI Overlay (https://github.com/OAI/Overlay-Specification) to manipulate the OpenAPI specification before generation. This allows modifying the specification without needing to apply changes directly to it, making it easier to keep it up-to-date.",
"properties": {
"path": {
"description": "The path to the Overlay file",
"type": "string"
},
"strict": {
"type": "boolean",
"description": "Strict defines whether the Overlay should be applied in a strict way, highlighting any actions that will not take any effect. This can, however, lead to more work when testing new actions in an Overlay, so can be turned off with this setting.",
"default": true
}
},
"required": [
"path"
]
},
"yaml-tags": {
"type": "boolean",
"description": "Enable the generation of YAML tags for struct fields"
},
"client-response-bytes-function": {
"type": "boolean",
"description": "Enable the generation of a `Bytes()` method on response objects for `ClientWithResponses`"
},
"prefer-skip-optional-pointer": {
"type": "boolean",
"description": "Allows defining at a global level whether to omit the pointer for a type to indicate that the field/type is optional. This is the same as adding `x-go-type-skip-optional-pointer` to each field (manually, or using an OpenAPI Overlay). A field can set `x-go-type-skip-optional-pointer: false` to still require the optional pointer.",
"default": false
},
"prefer-skip-optional-pointer-on-container-types": {
"type": "boolean",
"description": "Allows disabling the generation of an 'optional pointer' for an optional field that is a container type (such as a slice or a map), which ends up requiring an additional, unnecessary, `... != nil` check. A field can set `x-go-type-skip-optional-pointer: false` to still require the optional pointer.",
"default": false
}
}
},
"import-mapping": {
"type": "object",
"additionalProperties": {
"type": "string",
"description": "ImportMapping specifies the golang package path for each external reference. A value of `-` will indicate that the current package will be used"
}
},
"additional-imports": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"alias": {
"type": "string"
},
"package": {
"type": "string"
}
},
"required": [
"package"
]
},
"description": "AdditionalImports defines any additional Go imports to add to the generated code"
},
"output": {
"type": "string",
"description": "The filename to output"
}
},
"required": [
"package",
"output"
]
}
Generating with github.com/atombender/go-jsonschema
The most commonly used generator I've seen is github.com/atombender/go-jsonschema
, which is found under omissis/go-jsonschema
on GitHub.
For instance, we can use go tool
functionality to add it to our go.mod
:
go get -tool github.com/atombender/go-jsonschema
Then, to generate, we need to wire it in i.e.
package atombender
// NOTE the `--only-models` for this specific case
//go:generate go tool go-jsonschema ../oapi-codegen-schema.json --schema-package https://github.com/oapi-codegen/oapi-codegen/blob/v2.4.1/configuration-schema.json=atombender --schema-output https://github.com/oapi-codegen/oapi-codegen/blob/v2.4.1/configuration-schema.json=models.gen.go --only-models --tags yaml
This can seem a little verbose when "only" working with a single document but I can see that when working with multi-file schemas - as we've seen with oapi-codegen
- there are benefits to being able to manage the generation + output paths for each accordingly.
Note that to use go-jsonschema
, we need to specify the $id
in the schema, so we can then refer to it above ππΌ, with a change like so:
{
"$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "https://github.com/oapi-codegen/oapi-codegen/blob/v2.4.1/configuration-schema.json",
"description": "Configuration files for oapi-codegen",
"type": "object",
This then generates the following Go code:
// Code generated by github.com/atombender/go-jsonschema, DO NOT EDIT.
package atombender
// Configuration files for oapi-codegen
type OapiCodegenSchemaJson struct {
// AdditionalImports defines any additional Go imports to add to the generated
// code
AdditionalImports []OapiCodegenSchemaJsonAdditionalImportsElem `yaml:"additional-imports,omitempty"`
// Compatibility corresponds to the JSON schema field "compatibility".
Compatibility *OapiCodegenSchemaJsonCompatibility `yaml:"compatibility,omitempty"`
// Generate specifies which supported output formats to generate
Generate *OapiCodegenSchemaJsonGenerate `yaml:"generate,omitempty"`
// ImportMapping corresponds to the JSON schema field "import-mapping".
ImportMapping map[string]string `yaml:"import-mapping,omitempty"`
// The filename to output
Output string `yaml:"output"`
// OutputOptions are used to modify the output code in some way
OutputOptions *OapiCodegenSchemaJsonOutputOptions `yaml:"output-options,omitempty"`
// Go package name to generate the code under
Package string `yaml:"package"`
}
type OapiCodegenSchemaJsonAdditionalImportsElem struct {
// Alias corresponds to the JSON schema field "alias".
Alias *string `yaml:"alias,omitempty"`
// Package corresponds to the JSON schema field "package".
Package string `yaml:"package"`
}
type OapiCodegenSchemaJsonCompatibility struct {
// AllowUnexportedStructFieldNames makes it possible to output structs that have
// fields that are unexported.
// This is expected to be used in conjunction with an extension such as
// `x-go-name` to override the output name, and `x-oapi-codegen-extra-tags` to not
// produce JSON tags for `encoding/json`.
// NOTE that this can be confusing to users of your OpenAPI specification, who may
// see a field present and therefore be expecting to see it in the response,
// without understanding the nuance of how `oapi-codegen` generates the code.
AllowUnexportedStructFieldNames *bool `yaml:"allow-unexported-struct-field-names,omitempty"`
// When set to true, always prefix enum values with their type name instead of
// only when typenames would be conflicting.
AlwaysPrefixEnumValues *bool `yaml:"always-prefix-enum-values,omitempty"`
// Our generated code for Chi has historically inverted the order in which Chi
// middleware is applied such that the last invoked middleware ends up executing
// first in the Chi chain This resolves the behavior such that middlewares are
// chained in the order they are invoked. Please see
// https://github.com/oapi-codegen/oapi-codegen/issues/786
ApplyChiMiddlewareFirstToLast *bool `yaml:"apply-chi-middleware-first-to-last,omitempty"`
// Our generated code for gorilla/mux has historically inverted the order in which
// gorilla/mux middleware is applied such that the last invoked middleware ends up
// executing first in the middlewares chain This resolves the behavior such that
// middlewares are chained in the order they are invoked. Please see
// https://github.com/oapi-codegen/oapi-codegen/issues/841
ApplyGorillaMiddlewareFirstToLast *bool `yaml:"apply-gorilla-middleware-first-to-last,omitempty"`
// DEPRECATED: No longer used.
// CircularReferenceLimit allows controlling the limit for circular reference
// checking. In some OpenAPI specifications, we have a higher number of circular
// references than is allowed out-of-the-box, but can be tuned to allow traversing
// them.
CircularReferenceLimit *int `yaml:"circular-reference-limit,omitempty"`
// When an object contains no members, and only an additionalProperties
// specification, it is flattened to a map
DisableFlattenAdditionalProperties *bool `yaml:"disable-flatten-additional-properties,omitempty"`
// When an object property is both required and readOnly the go model is generated
// as a pointer. Set DisableRequiredReadOnlyAsPointer to true to mark them as non
// pointer. Please see https://github.com/oapi-codegen/oapi-codegen/issues/604
DisableRequiredReadonlyAsPointer *bool `yaml:"disable-required-readonly-as-pointer,omitempty"`
// It was a mistake to generate a go type definition for every $ref in the OpenAPI
// schema. New behavior uses type aliases where possible, but this can generate
// code which breaks existing builds. Set OldAliasing to true for old behavior.
// Please see https://github.com/oapi-codegen/oapi-codegen/issues/549
OldAliasing *bool `yaml:"old-aliasing,omitempty"`
// Enum values can generate conflicting typenames, so we've updated the code for
// enum generation to avoid these conflicts, but it will result in some enum types
// being renamed in existing code. Set OldEnumConflicts to true to revert to old
// behavior. Please see: Please see
// https://github.com/oapi-codegen/oapi-codegen/issues/549
OldEnumConflicts *bool `yaml:"old-enum-conflicts,omitempty"`
// In the past, we merged schemas for `allOf` by inlining each schema within the
// schema list. This approach, though, is incorrect because `allOf` merges at the
// schema definition level, not at the resulting model level. So, new behavior
// merges OpenAPI specs but generates different code than we have in the past. Set
// OldMergeSchemas to true for the old behavior. Please see
// https://github.com/oapi-codegen/oapi-codegen/issues/531
OldMergeSchemas *bool `yaml:"old-merge-schemas,omitempty"`
// When `oapi-codegen` parses the original OpenAPI specification, it will apply
// the configured `output-options.name-normalizer` to each operation's
// `operationId` before that is used to generate code from.
// However, this is also applied to the copy of the `operationId`s in the
// `embedded-spec` generation, which means that the embedded OpenAPI specification
// is then out-of-sync with the input specificiation.
// To ensure that the `operationId` in the embedded spec is preserved as-is from
// the input specification, set this. NOTE that this will not impact generated
// code.
// NOTE that if you're using `include-operation-ids` or `exclude-operation-ids`
// you may want to ensure that the `operationId`s used are correct.
PreserveOriginalOperationIdCasingInEmbeddedSpec *bool `yaml:"preserve-original-operation-id-casing-in-embedded-spec,omitempty"`
}
// Generate specifies which supported output formats to generate
type OapiCodegenSchemaJsonGenerate struct {
// ChiServer specifies whether to generate chi server boilerplate
ChiServer *bool `yaml:"chi-server,omitempty"`
// Client specifies whether to generate client boilerplate
Client *bool `yaml:"client,omitempty"`
// EchoServer specifies whether to generate echo server boilerplate
EchoServer *bool `yaml:"echo-server,omitempty"`
// EmbeddedSpec indicates whether to embed the swagger spec in the generated code
EmbeddedSpec *bool `yaml:"embedded-spec,omitempty"`
// FiberServer specifies whether to generate fiber server boilerplate
FiberServer *bool `yaml:"fiber-server,omitempty"`
// GinServer specifies whether to generate gin server boilerplate
GinServer *bool `yaml:"gin-server,omitempty"`
// GorillaServer specifies whether to generate Gorilla server boilerplate
GorillaServer *bool `yaml:"gorilla-server,omitempty"`
// IrisServer specifies whether to generate iris server boilerplate
IrisServer *bool `yaml:"iris-server,omitempty"`
// Models specifies whether to generate type definitions
Models *bool `yaml:"models,omitempty"`
// StdHTTPServer specifies whether to generate stdlib http server boilerplate
StdHttpServer *bool `yaml:"std-http-server,omitempty"`
// Strict specifies whether to generate strict server wrapper
StrictServer *bool `yaml:"strict-server,omitempty"`
}
// OutputOptions are used to modify the output code in some way
type OapiCodegenSchemaJsonOutputOptions struct {
// AdditionalInitialisms defines additional initialisms to be used by the code
// generator. Has no effect unless the `name-normalizer` is set to
// `ToCamelCaseWithInitialisms`
AdditionalInitialisms []string `yaml:"additional-initialisms,omitempty"`
// Enable the generation of a `Bytes()` method on response objects for
// `ClientWithResponses`
ClientResponseBytesFunction *bool `yaml:"client-response-bytes-function,omitempty"`
// Override the default generated client type with the value
ClientTypeName *string `yaml:"client-type-name,omitempty"`
// DisableTypeAliasesForType allows defining which OpenAPI `type`s will explicitly
// not use type aliases
DisableTypeAliasesForType []OapiCodegenSchemaJsonOutputOptionsDisableTypeAliasesForTypeElem `yaml:"disable-type-aliases-for-type,omitempty"`
// Exclude operations that have one of these operation-ids. Ignored when empty.
ExcludeOperationIds []string `yaml:"exclude-operation-ids,omitempty"`
// Exclude from generation schemas with given names. Ignored when empty.
ExcludeSchemas []string `yaml:"exclude-schemas,omitempty"`
// Exclude operations that have one of these tags. Ignored when empty.
ExcludeTags []string `yaml:"exclude-tags,omitempty"`
// Only include operations that have one of these operation-ids. Ignored when
// empty.
IncludeOperationIds []string `yaml:"include-operation-ids,omitempty"`
// Only include operations that have one of these tags. Ignored when empty.
IncludeTags []string `yaml:"include-tags,omitempty"`
// Whether to use the initialism overrides
InitialismOverrides *bool `yaml:"initialism-overrides,omitempty"`
// NameNormalizer is the method used to normalize Go names and types, for instance
// converting the text `MyApi` to `MyAPI`. Corresponds with the constants defined
// for `codegen.NameNormalizerFunction`
NameNormalizer OapiCodegenSchemaJsonOutputOptionsNameNormalizer `yaml:"name-normalizer,omitempty"`
// Whether to generate nullable type for nullable fields
NullableType *bool `yaml:"nullable-type,omitempty"`
// Overlay defines configuration for the OpenAPI Overlay
// (https://github.com/OAI/Overlay-Specification) to manipulate the OpenAPI
// specification before generation. This allows modifying the specification
// without needing to apply changes directly to it, making it easier to keep it
// up-to-date.
Overlay *OapiCodegenSchemaJsonOutputOptionsOverlay `yaml:"overlay,omitempty"`
// Allows defining at a global level whether to omit the pointer for a type to
// indicate that the field/type is optional. This is the same as adding
// `x-go-type-skip-optional-pointer` to each field (manually, or using an OpenAPI
// Overlay). A field can set `x-go-type-skip-optional-pointer: false` to still
// require the optional pointer.
PreferSkipOptionalPointer bool `yaml:"prefer-skip-optional-pointer,omitempty"`
// Allows disabling the generation of an 'optional pointer' for an optional field
// that is a container type (such as a slice or a map), which ends up requiring an
// additional, unnecessary, `... != nil` check. A field can set
// `x-go-type-skip-optional-pointer: false` to still require the optional pointer.
PreferSkipOptionalPointerOnContainerTypes bool `yaml:"prefer-skip-optional-pointer-on-container-types,omitempty"`
// The suffix used for responses types
ResponseTypeSuffix *string `yaml:"response-type-suffix,omitempty"`
// Whether to skip go imports on the generated code
SkipFmt *bool `yaml:"skip-fmt,omitempty"`
// Whether to skip pruning unused components on the generated code
SkipPrune *bool `yaml:"skip-prune,omitempty"`
// Override built-in templates from user-provided files
UserTemplates map[string]string `yaml:"user-templates,omitempty"`
// Enable the generation of YAML tags for struct fields
YamlTags *bool `yaml:"yaml-tags,omitempty"`
}
type OapiCodegenSchemaJsonOutputOptionsDisableTypeAliasesForTypeElem string
const OapiCodegenSchemaJsonOutputOptionsDisableTypeAliasesForTypeElemArray OapiCodegenSchemaJsonOutputOptionsDisableTypeAliasesForTypeElem = "array"
type OapiCodegenSchemaJsonOutputOptionsNameNormalizer string
const OapiCodegenSchemaJsonOutputOptionsNameNormalizerToCamelCase OapiCodegenSchemaJsonOutputOptionsNameNormalizer = "ToCamelCase"
const OapiCodegenSchemaJsonOutputOptionsNameNormalizerToCamelCaseWithDigits OapiCodegenSchemaJsonOutputOptionsNameNormalizer = "ToCamelCaseWithDigits"
const OapiCodegenSchemaJsonOutputOptionsNameNormalizerToCamelCaseWithInitialisms OapiCodegenSchemaJsonOutputOptionsNameNormalizer = "ToCamelCaseWithInitialisms"
// Overlay defines configuration for the OpenAPI Overlay
// (https://github.com/OAI/Overlay-Specification) to manipulate the OpenAPI
// specification before generation. This allows modifying the specification without
// needing to apply changes directly to it, making it easier to keep it up-to-date.
type OapiCodegenSchemaJsonOutputOptionsOverlay struct {
// The path to the Overlay file
Path string `yaml:"path"`
// Strict defines whether the Overlay should be applied in a strict way,
// highlighting any actions that will not take any effect. This can, however, lead
// to more work when testing new actions in an Overlay, so can be turned off with
// this setting.
Strict bool `yaml:"strict,omitempty"`
}
A few things to note:
- The filename is used as a prefix for all types - it doesn't seem possible to override it
- I.e.
oapi-codegen-schema.json
becomesOapiCodegenSchemaJson
- I.e.
- This generates values for
enum
s - This will generate, by default,
json
,yaml
andmapstructure
struct tags (but can be overridden, as seen above) - The
description
field for a given field/type will be used verbatim so it may be worth rewriting them as if they're Go doc comments - This only supports
draft-07
of JSON Schema, so misses out on a number of features added into newer drafts of JSON Schema
Generating with git.sr.ht/~emersion/go-jsonschema
If you want to generate code using a newer JSON Schema draft, such as 2020-12
, emersion's package is for you.
For instance, we can add it like so:
go get -tool git.sr.ht/~emersion/go-jsonschema/cmd/jsonschemagen
Then, to generate, we need to wire it in i.e.
package emersion
//go:generate go tool jsonschemagen -s ../oapi-codegen-schema.json -o models.gen.go
This then generates the following Go code:
package emersion
import "encoding/json"
type Root struct {
AdditionalImports []*struct {
Alias string `json:"alias,omitempty"`
Package string `json:"package"`
} `json:"additional-imports,omitempty"`
Compatibility *struct {
AllowUnexportedStructFieldNames bool `json:"allow-unexported-struct-field-names,omitempty"`
AlwaysPrefixEnumValues bool `json:"always-prefix-enum-values,omitempty"`
ApplyChiMiddlewareFirstToLast bool `json:"apply-chi-middleware-first-to-last,omitempty"`
ApplyGorillaMiddlewareFirstToLast bool `json:"apply-gorilla-middleware-first-to-last,omitempty"`
CircularReferenceLimit int64 `json:"circular-reference-limit,omitempty"`
DisableFlattenAdditionalProperties bool `json:"disable-flatten-additional-properties,omitempty"`
DisableRequiredReadonlyAsPointer bool `json:"disable-required-readonly-as-pointer,omitempty"`
OldAliasing bool `json:"old-aliasing,omitempty"`
OldEnumConflicts bool `json:"old-enum-conflicts,omitempty"`
OldMergeSchemas bool `json:"old-merge-schemas,omitempty"`
PreserveOriginalOperationIdCasingInEmbeddedSpec bool `json:"preserve-original-operation-id-casing-in-embedded-spec,omitempty"`
} `json:"compatibility,omitempty"`
Generate *struct {
ChiServer bool `json:"chi-server,omitempty"`
Client bool `json:"client,omitempty"`
EchoServer bool `json:"echo-server,omitempty"`
EmbeddedSpec bool `json:"embedded-spec,omitempty"`
FiberServer bool `json:"fiber-server,omitempty"`
GinServer bool `json:"gin-server,omitempty"`
GorillaServer bool `json:"gorilla-server,omitempty"`
IrisServer bool `json:"iris-server,omitempty"`
Models bool `json:"models,omitempty"`
StdHttpServer bool `json:"std-http-server,omitempty"`
StrictServer bool `json:"strict-server,omitempty"`
} `json:"generate,omitempty"`
ImportMapping map[string]string `json:"import-mapping,omitempty"`
Output string `json:"output"`
OutputOptions *struct {
AdditionalInitialisms []string `json:"additional-initialisms,omitempty"`
ClientResponseBytesFunction bool `json:"client-response-bytes-function,omitempty"`
ClientTypeName string `json:"client-type-name,omitempty"`
DisableTypeAliasesForType []string `json:"disable-type-aliases-for-type,omitempty"`
ExcludeOperationIds []string `json:"exclude-operation-ids,omitempty"`
ExcludeSchemas []string `json:"exclude-schemas,omitempty"`
ExcludeTags []string `json:"exclude-tags,omitempty"`
IncludeOperationIds []string `json:"include-operation-ids,omitempty"`
IncludeTags []string `json:"include-tags,omitempty"`
InitialismOverrides bool `json:"initialism-overrides,omitempty"`
NameNormalizer string `json:"name-normalizer,omitempty"`
NullableType bool `json:"nullable-type,omitempty"`
Overlay map[string]json.RawMessage `json:"overlay,omitempty"`
PreferSkipOptionalPointer bool `json:"prefer-skip-optional-pointer,omitempty"`
PreferSkipOptionalPointerOnContainerTypes bool `json:"prefer-skip-optional-pointer-on-container-types,omitempty"`
ResponseTypeSuffix string `json:"response-type-suffix,omitempty"`
SkipFmt bool `json:"skip-fmt,omitempty"`
SkipPrune bool `json:"skip-prune,omitempty"`
UserTemplates map[string]string `json:"user-templates,omitempty"`
YamlTags bool `json:"yaml-tags,omitempty"`
} `json:"output-options,omitempty"`
Package string `json:"package"`
}
A few things to note:
- This creates a big ol' single struct with anonymous types
- This doesn't seem to wire in fields'
description
s - This doesn't generate
const
s forenum
s- Aside: this reminds me I've still got a patch I need to finish up contributing!
- Some fields generate as
json.RawMessage
as they're not matched with a type
Tips
As a general tip, take advantage of the usage of $refs
and definitions in JSON Schema to provide a hint that there should be a named type, instead of an anonymous type.
JSON Schema draft-07
In draft-07
, we'd make a change such as:
"description": "Go package name to generate the code under"
},
"generate": {
- "type": "object",
- "additionalProperties": false,
- "description": "Generate specifies which supported output formats to generate",
- "properties": {
- "iris-server": {
- "type": "boolean",
- "description": "IrisServer specifies whether to generate iris server boilerplate"
- },
- "chi-server": {
- "type": "boolean",
- "description": "ChiServer specifies whether to generate chi server boilerplate"
- },
- "fiber-server": {
- "type": "boolean",
- "description": "FiberServer specifies whether to generate fiber server boilerplate"
- },
- "echo-server": {
- "type": "boolean",
- "description": "EchoServer specifies whether to generate echo server boilerplate"
- },
- "gin-server": {
- "type": "boolean",
- "description": "GinServer specifies whether to generate gin server boilerplate"
- },
- "gorilla-server": {
- "type": "boolean",
- "description": "GorillaServer specifies whether to generate Gorilla server boilerplate"
- },
- "std-http-server": {
- "type": "boolean",
- "description": "StdHTTPServer specifies whether to generate stdlib http server boilerplate"
- },
- "strict-server": {
- "type": "boolean",
- "description": "Strict specifies whether to generate strict server wrapper"
- },
- "client": {
- "type": "boolean",
- "description": "Client specifies whether to generate client boilerplate"
- },
- "models": {
- "type": "boolean",
- "description": "Models specifies whether to generate type definitions"
- },
- "embedded-spec": {
- "type": "boolean",
- "description": "EmbeddedSpec indicates whether to embed the swagger spec in the generated code"
- }
- }
+ "$ref": "#/definitions/GenerateOptions"
},
"compatibility": {
"type": "object",
@@ -278,6 +230,59 @@
"description": "The filename to output"
}
},
+ "definitions": {
+ "GenerateOptions": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "Generate specifies which supported output formats to generate",
+ "properties": {
+ "iris-server": {
+ "type": "boolean",
+ "description": "IrisServer specifies whether to generate iris server boilerplate"
+ },
+ "chi-server": {
+ "type": "boolean",
+ "description": "ChiServer specifies whether to generate chi server boilerplate"
+ },
+ "fiber-server": {
+ "type": "boolean",
+ "description": "FiberServer specifies whether to generate fiber server boilerplate"
+ },
+ "echo-server": {
+ "type": "boolean",
+ "description": "EchoServer specifies whether to generate echo server boilerplate"
+ },
+ "gin-server": {
+ "type": "boolean",
+ "description": "GinServer specifies whether to generate gin server boilerplate"
+ },
+ "gorilla-server": {
+ "type": "boolean",
+ "description": "GorillaServer specifies whether to generate Gorilla server boilerplate"
+ },
+ "std-http-server": {
+ "type": "boolean",
+ "description": "StdHTTPServer specifies whether to generate stdlib http server boilerplate"
+ },
+ "strict-server": {
+ "type": "boolean",
+ "description": "Strict specifies whether to generate strict server wrapper"
+ },
+ "client": {
+ "type": "boolean",
+ "description": "Client specifies whether to generate client boilerplate"
+ },
+ "models": {
+ "type": "boolean",
+ "description": "Models specifies whether to generate type definitions"
+ },
+ "embedded-spec": {
+ "type": "boolean",
+ "description": "EmbeddedSpec indicates whether to embed the swagger spec in the generated code"
+ }
+ }
+ }
+ },
"required": [
"package",
"output"
Which when using github.com/atombender/go-jsonschema
will generate:
+// Generate specifies which supported output formats to generate
+type GenerateOptions struct {
+ // ChiServer specifies whether to generate chi server boilerplate
+ ChiServer *bool `yaml:"chi-server,omitempty"`
+
+ // Client specifies whether to generate client boilerplate
+ Client *bool `yaml:"client,omitempty"`
+
+ // EchoServer specifies whether to generate echo server boilerplate
+ EchoServer *bool `yaml:"echo-server,omitempty"`
+
+ // EmbeddedSpec indicates whether to embed the swagger spec in the generated code
+ EmbeddedSpec *bool `yaml:"embedded-spec,omitempty"`
+
+ // FiberServer specifies whether to generate fiber server boilerplate
+ FiberServer *bool `yaml:"fiber-server,omitempty"`
+
+ // GinServer specifies whether to generate gin server boilerplate
+ GinServer *bool `yaml:"gin-server,omitempty"`
+
+ // GorillaServer specifies whether to generate Gorilla server boilerplate
+ GorillaServer *bool `yaml:"gorilla-server,omitempty"`
+
+ // IrisServer specifies whether to generate iris server boilerplate
+ IrisServer *bool `yaml:"iris-server,omitempty"`
+
+ // Models specifies whether to generate type definitions
+ Models *bool `yaml:"models,omitempty"`
+
+ // StdHTTPServer specifies whether to generate stdlib http server boilerplate
+ StdHttpServer *bool `yaml:"std-http-server,omitempty"`
+
+ // Strict specifies whether to generate strict server wrapper
+ StrictServer *bool `yaml:"strict-server,omitempty"`
+}
+
// Configuration files for oapi-codegen
type OapiCodegenSchemaJson struct {
// AdditionalImports defines any additional Go imports to add to the generated
@@ -11,8 +47,8 @@ type OapiCodegenSchemaJson struct {
// Compatibility corresponds to the JSON schema field "compatibility".
Compatibility *OapiCodegenSchemaJsonCompatibility `yaml:"compatibility,omitempty"`
- // Generate specifies which supported output formats to generate
- Generate *OapiCodegenSchemaJsonGenerate `yaml:"generate,omitempty"`
+ // Generate corresponds to the JSON schema field "generate".
+ Generate *GenerateOptions `yaml:"generate,omitempty"`
JSON Schema 2020-12
In 2020-12
, we'd make a change such as:
@@ -10,55 +10,7 @@
"description": "Go package name to generate the code under"
},
"generate": {
- "type": "object",
- "additionalProperties": false,
- "description": "Generate specifies which supported output formats to generate",
- "properties": {
- "iris-server": {
- "type": "boolean",
- "description": "IrisServer specifies whether to generate iris server boilerplate"
- },
- "chi-server": {
- "type": "boolean",
- "description": "ChiServer specifies whether to generate chi server boilerplate"
- },
- "fiber-server": {
- "type": "boolean",
- "description": "FiberServer specifies whether to generate fiber server boilerplate"
- },
- "echo-server": {
- "type": "boolean",
- "description": "EchoServer specifies whether to generate echo server boilerplate"
- },
- "gin-server": {
- "type": "boolean",
- "description": "GinServer specifies whether to generate gin server boilerplate"
- },
- "gorilla-server": {
- "type": "boolean",
- "description": "GorillaServer specifies whether to generate Gorilla server boilerplate"
- },
- "std-http-server": {
- "type": "boolean",
- "description": "StdHTTPServer specifies whether to generate stdlib http server boilerplate"
- },
- "strict-server": {
- "type": "boolean",
- "description": "Strict specifies whether to generate strict server wrapper"
- },
- "client": {
- "type": "boolean",
- "description": "Client specifies whether to generate client boilerplate"
- },
- "models": {
- "type": "boolean",
- "description": "Models specifies whether to generate type definitions"
- },
- "embedded-spec": {
- "type": "boolean",
- "description": "EmbeddedSpec indicates whether to embed the swagger spec in the generated code"
- }
- }
+ "$ref": "#/$defs/GenerateOptions"
},
"compatibility": {
"type": "object",
@@ -278,6 +230,59 @@
"description": "The filename to output"
}
},
+ "$defs": {
+ "GenerateOptions": {
+ "type": "object",
+ "additionalProperties": false,
+ "description": "Generate specifies which supported output formats to generate",
+ "properties": {
+ "iris-server": {
+ "type": "boolean",
+ "description": "IrisServer specifies whether to generate iris server boilerplate"
+ },
+ "chi-server": {
+ "type": "boolean",
+ "description": "ChiServer specifies whether to generate chi server boilerplate"
+ },
+ "fiber-server": {
+ "type": "boolean",
+ "description": "FiberServer specifies whether to generate fiber server boilerplate"
+ },
+ "echo-server": {
+ "type": "boolean",
+ "description": "EchoServer specifies whether to generate echo server boilerplate"
+ },
+ "gin-server": {
+ "type": "boolean",
+ "description": "GinServer specifies whether to generate gin server boilerplate"
+ },
+ "gorilla-server": {
+ "type": "boolean",
+ "description": "GorillaServer specifies whether to generate Gorilla server boilerplate"
+ },
+ "std-http-server": {
+ "type": "boolean",
+ "description": "StdHTTPServer specifies whether to generate stdlib http server boilerplate"
+ },
+ "strict-server": {
+ "type": "boolean",
+ "description": "Strict specifies whether to generate strict server wrapper"
+ },
+ "client": {
+ "type": "boolean",
+ "description": "Client specifies whether to generate client boilerplate"
+ },
+ "models": {
+ "type": "boolean",
+ "description": "Models specifies whether to generate type definitions"
+ },
+ "embedded-spec": {
+ "type": "boolean",
+ "description": "EmbeddedSpec indicates whether to embed the swagger spec in the generated code"
+ }
+ }
+ }
+ },
"required": [
"package",
"output"
This would then generate the following:
package emersion
import "encoding/json"
type Root struct {
// ...
Generate *GenerateOptions `json:"generate,omitempty"`
// ...
}
type GenerateOptions struct {
ChiServer bool `json:"chi-server,omitempty"`
Client bool `json:"client,omitempty"`
EchoServer bool `json:"echo-server,omitempty"`
EmbeddedSpec bool `json:"embedded-spec,omitempty"`
FiberServer bool `json:"fiber-server,omitempty"`
GinServer bool `json:"gin-server,omitempty"`
GorillaServer bool `json:"gorilla-server,omitempty"`
IrisServer bool `json:"iris-server,omitempty"`
Models bool `json:"models,omitempty"`
StdHttpServer bool `json:"std-http-server,omitempty"`
StrictServer bool `json:"strict-server,omitempty"`
}
Final thoughts
I appreciate the work that folks have put into creating tooling to support the different JSON Schema drafts - they make it much easier to use JSON Schema with Go!
Although not shown, github.com/atombender/go-jsonschema
is able to generate code to wire in default
values, as well as generate validation methods, which is pretty cool.
I do, however, each time I come to this wish that we added support in oapi-codegen
to also work more generally i.e. with JSON Schema (although I know that OpenAPI 3.0, which is all we support in oapi-codegen
, isn't true JSON Schema) because there are a number of edge cases I know we manage well from these sorts of schemas!