| name | create-openapi-schemas-from-golang-models |
| description | Create OpenAPI schemas from Golang models in Layer5 Cloud, generating schema artifacts in Meshery Schemas repository. Handles cross-repository schema creation workflow with proper naming conventions, Go code generation, and backwards compatibility validation. |
Create OpenAPI Schemas from Golang Models
Canonical naming contract — see docs/identifier-naming-contributor-guide.md in meshery/schemas (https://github.com/meshery/schemas/blob/master/docs/identifier-naming-contributor-guide.md) for the full directory (26-row naming table with before/after and do/don't examples). The inline rules below remain the skill's authority for its workflow scope; the guide is the reader-friendly cross-repo reference.
Overview
This skill guides you through creating OpenAPI schemas from existing Golang models in Layer5 Cloud (layer5io/meshery-cloud), with the generated schemas stored in Meshery Schemas (meshery/schemas). This cross-repository workflow ensures consistent API contracts between the two projects.
Source of truth depends on migration stage. While a construct is being migrated from layer5io/meshery-cloud, the downstream Golang models are the reference for field discovery (field names, types, JSON tags, DB column mappings). Once the construct has been fully migrated and its schema is defined in meshery/schemas, this repository becomes the permanent, authoritative source of truth. From that point, the design principles, naming conventions, type patterns, and structural rules defined here take precedence. If a downstream implementation diverges from the conventions here, make the schema correct and open issues in affected repositories documenting the required migration.
Prerequisites
Required Repositories
Both repositories must be locally cloned and available:
Required Knowledge
- Read the
meshery/schemas README.md and uphold all directives, including naming conventions
- Read AGENTS.md in both repositories
- During migration, treat the Golang models in
layer5io/meshery-cloud as the reference for field discovery (field names, types, JSON tags, DB mappings)
- After migration, meshery/schemas becomes the permanent source of truth — apply its design principles, naming conventions, and type patterns even when they conflict with downstream implementations
- When cross-construct consistency requires a breaking change to downstream code, make the change here and document the breakage in an issue on the affected repository
Naming Conventions
OpenAPI Schema Naming
| Element | Convention | Example |
|---|
| Property names | Preserve published wire-format casing: DB-backed fields use exact snake_case db names; new non-DB fields use camelCase | first_name, organization_id, schemaVersion |
| Identifier fields | camelCase + "Id" suffix | roleId, userId, keychainId |
| Enums | lowercase | admin, user, enabled |
| Object names | singular nouns | role, keychain, user |
| Schema components | PascalCase | Role, RolesPage, RoleHolderRequest |
| Files/folders | lowercase | role.yaml, api.yml |
Endpoint Naming
| Element | Convention | Example |
|---|
| Paths | /api + kebab-case plurals | /api/identity/orgs/{orgId}/roles |
| Operations | camelCase VerbNoun | GetAllRoles, UpsertRoles |
If a property has x-oapi-codegen-extra-tags.db and that db value is snake_case, the schema property name and JSON tag must use that exact snake_case name. Do not camelize DB-backed fields in-place within an existing API version. Pagination envelopes must use page, page_size, and total_count.
| Non-CRUD actions | append verb | .../keychains/{keychainId} |
Response descriptions and response message text must not include the word successfully. Use neutral wording such as Role deleted, Webhook processed, or Roles response.
Workflow
Task 1: Create Schema Artifacts
In meshery/schemas, create these files for the target construct:
Dual-Schema Pattern (required): Every entity needs two schemas — <construct>.yaml (full response entity) and {Construct}Payload in api.yml (write request body). Never use the entity schema as a POST/PUT requestBody. See AGENTS.md § "The Dual-Schema Pattern" for full rules.
schemas/constructs/v1beta1/<construct_name>/
├── <construct_name>.yaml # Schema definition
├── api.yml # Endpoint definitions + page/payload schemas
└── templates/
└── <construct_name>_template.json # Example usage
Schema Definition (<construct_name>.yaml)
This is the response schema — the full persisted object as the API returns it. It must:
- Have
additionalProperties: false at the top level
- Include all server-generated fields (
id, created_at, updated_at, deleted_at) in properties and required
Map Golang struct fields to OpenAPI properties:
type: object
additionalProperties: false
required:
- id
- roleName
- created_at
- updated_at
properties:
id:
type: string
format: uuid
x-oapi-codegen-extra-tags:
json: "id,omitempty"
yaml: "id,omitempty"
db: "id"
roleName:
type: string
x-oapi-codegen-extra-tags:
json: "role_name,omitempty"
yaml: "role_name,omitempty"
db: "role_name"
description:
type: string
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
deletedAt:
type: string
format: date-time
nullable: true
API Definition (api.yml)
Define endpoints matching the Layer5 Cloud router. All POST/PUT operations must use a {Construct}Payload request body — never the full entity schema.
openapi: 3.0.3
info:
title: Role API
version: v1beta1
paths:
/api/identity/orgs/{orgId}/roles:
get:
operationId: getAllRoles
parameters:
- name: orgId
in: path
required: true
schema:
type: string
format: uuid
- name: page
in: query
schema:
type: integer
- name: pagesize
in: query
schema:
type: integer
responses:
'200':
description: List of roles
content:
application/json:
schema:
$ref: '#/components/schemas/RolesPage'
post:
operationId: upsertRole
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RolePayload'
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Role'
components:
schemas:
Role:
$ref: './role.yaml'
RolePayload:
type: object
description: Payload for creating or updating a role.
required:
- roleName
properties:
id:
type: string
format: uuid
x-oapi-codegen-extra-tags:
json: "id,omitempty"
roleName:
type: string
description:
type: string
RolesPage:
type: object
properties:
page:
type: integer
roles:
type: array
items:
$ref: '#/components/schemas/Role'
totalCount:
type: integer
pageSize:
type: integer
Go Type Hints
For cross-schema references, use x-go-type to avoid redundant structs:
registrant:
x-go-type: "RegistrantReference"
x-go-import-path: "github.com/meshery/schemas/models/v1beta1/core"
$ref: "#/components/schemas/RegistrantReference"
Task 2: Generate Go Models
In the meshery/schemas repository:
cd /Users/l/code/schemas
make generate-golang
Verification Steps:
-
Ensure generated models are in models/v1beta1/<construct_name>/
-
If a schema type within the construct is stored as a JSON blob in a database column and has a dedicated schema definition with explicit properties, add x-generate-db-helpers: true at the schema component level in api.yml. This instructs the generator to produce Scan() and Value() SQL driver methods automatically in zz_generated.helpers.go — no manual helpers.go is needed for that type.
Quiz:
x-generate-db-helpers: true
type: object
properties:
id:
$ref: "../../v1alpha1/core/api.yml#/components/schemas/uuid"
title:
type: string
For amorphous JSON blob fields that lack a fixed schema definition (e.g., a freeform metadata map), use x-go-type: "core.Map" on the property instead — do not use x-generate-db-helpers for those.
-
If helpers are still needed for non-generated behavior (e.g., TableName(), custom business logic), create helpers.go manually. When implementing Scan/Value, follow these rules (see docs/schema-authoring-reference.md § "SQL Driver (Scan/Value) Implementation Rules"):
Value() must always marshal — never return (nil, nil). A nil map produces JSON "null", not SQL NULL.
Scan() must zero the receiver (*m = nil) when src is nil, not silently return.
package role
import (
"database/sql/driver"
"encoding/json"
)
func (r Role) TableName() string {
return "roles"
}
func (r Role) Value() (driver.Value, error) {
b, err := json.Marshal(r)
if err != nil {
return nil, err
}
return string(b), nil
}
- Create/update tests:
go test ./models/v1beta1/<construct_name>/...
- Run full test suite:
go test ./...
Task 3: Validate Cross-Construct Consistency
Compare generated models with Layer5 Cloud models to identify migration requirements:
Reference: layer5io/meshery-cloud/server/models/<construct>.go
Generated: meshery/schemas/models/v1beta1/<construct_name>/<construct_name>.go
Validation Checklist:
Common Patterns
Pagination Response
<Construct>Page:
type: object
properties:
page:
type: integer
data:
type: array
items:
$ref: '#/components/schemas/<Construct>'
totalCount:
type: integer
pageSize:
type: integer
UUID Fields
id:
type: string
format: uuid
x-oapi-codegen-extra-tags:
json: "id,omitempty"
db: "id"
Nullable Time Fields
deletedAt:
type: string
format: date-time
nullable: true
x-go-type: "sql.NullTime"
x-go-import-path: "database/sql"
String Arrays (pq.StringArray)
roleNames:
type: array
items:
type: string
x-go-type: "pq.StringArray"
x-go-import-path: "github.com/lib/pq"
Exemplar: Academy Construct
The Academy construct in meshery/schemas serves as the primary exemplar for this workflow. Study these files:
Reference Files
Key Patterns from Academy
- Cross-schema references using
$ref with external files
- Custom Go type mappings with
x-go-type and x-go-type-import
- Nullable fields using
core.NullTime
- Enum definitions with proper Go type hints
- SQL Scanner/Valuer implementations in helpers.go
Source Repository
Example: Role Construct
Source Files in Layer5 Cloud
- Model:
server/models/roles.go
- Handler:
server/handlers/roles.go
- DAO:
server/dao/roles_dao.go
- Routes:
server/router/router.go (search for "roles")
Key Endpoints
| Method | Path | Handler |
|---|
| GET | /api/identity/orgs/:orgID/roles | GetAllRoles |
| POST | /api/identity/orgs/:orgID/roles | UpsertRoles |
| GET | /api/identity/orgs/:orgID/roles/:roleID/keychains | GetKeychainsByRoleID |
| POST | /api/identity/orgs/:orgID/roles/:roleID/keychains/:keychainID | AssignKeychainToRole |
| DELETE | /api/identity/orgs/:orgID/roles/:roleID/keychains/:keychainID | UnAssignKeychainFromRole |
Key Models to Schema-ify
Role - Core role entity
RolesPage - Paginated response
UserWithRole - User with role assignments
RoleHolderRequest - Request payload for role assignment
Validation Commands
cd /Users/l/code/schemas
make validate-schemas
make build
go test ./models/v1beta1/<construct>/...
Tips
- Start with the Golang struct for field discovery — but apply meshery/schemas conventions to the schema design
- Match JSON tags exactly — critical for API compatibility
- Use
x-oapi-codegen-extra-tags — preserves db and yaml tags
- Check router for all endpoints — don't miss any routes
- Run
make build — validates everything at once
- meshery/schemas conventions take precedence — if a downstream type pattern (e.g., pointer vs non-pointer UUID) conflicts with the uniform pattern established across constructs here, follow the meshery/schemas pattern and document the breaking change