Generating Go code from JSON Schema documents

Featured image for sharing metadata for article

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 becomes OapiCodegenSchemaJson
  • This generates values for enums
  • This will generate, by default, json, yaml and mapstructure 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' descriptions
  • This doesn't generate consts for enums
  • 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!

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #go #json-schema.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.