ogen

repository·main·Indexed 24 days ago

https://github.com/ogen-go/ogen

An OpenAPI v3 code generator for Go focused on high performance and type safety. It generates statically typed clients and servers with optimized JSON encoding/decoding, avoiding reflection and interface{}. Key features include support for sum types (oneOf), generic wrappers for optional and nullable fields, complex uniqueItems validation, and custom OpenAPI extensions for naming, streaming, and validation.

Tokens
13.9K
Snippets
33
Records
46
Agent score
81%

What's inside ogen

  1. Use the uri package for OpenAPI URI template encoding/decoding

    main
    The uri package provides functionality for encoding and decoding parameters within URI templates, specifically implementing the OpenAPI subset of RFC 6570. This is useful when working with API paths that contain variable segments defined by URI templates.
  2. How ogen handles optional and nullable fields with generics

    main

    Instead of using pointers for optional or nullable fields, ogen generates generic wrapper types. This avoids pointer indirection and provides clearer semantics for the three states of a field:

    1. Optional: The field may be absent from the payload.
    2. Nullable: The field may be present but have a null value.
    3. Optional and Nullable: The field may be absent OR present as null.

    Commonly generated wrappers include Optional[T], Nullable[T], and OptionalNullable[T] (e.g., OptNilString).

    Example: OptNilString

    An OptNilString represents a string that is both optional and nullable.

    type OptNilString struct {
    	Value string
    	Set   bool
    	Null  bool
    }

    Helper Methods

    Generated wrappers include several convenience methods:

    • Get() (v T, ok bool): Returns the value and a boolean indicating if it was set.
    • IsNull() bool: Returns true if the value is null.
    • IsSet() bool: Returns true if the value was present in the payload.
    • IsEmpty() bool: Returns true if the value is the zero value.
    • New[Type](v T): A constructor function (e.g., NewOptNilString(v string)).
    // OptNilString is optional nullable string.
    type OptNilString struct {
    	Value string
    	Set   bool
    	Null  bool
    }
    
    func (OptNilString) Get() (v string, ok bool)
    func (OptNilString) IsNull() bool
    func (OptNilString) IsSet() bool
    func (OptNilString) IsEmpty() bool
    
    func NewOptNilString(v string) OptNilString
  3. Discriminator Inference strategies for oneOf

    main

    When generating sum types for oneOf schemas, ogen automatically determines how to distinguish between variants using one of the following strategies:

    1. Type-based discrimination: Used for primitive types. ogen checks the JSON type (e.g., string vs integer) at runtime.
    2. Explicit discriminator: If the OpenAPI schema defines a discriminator object with a propertyName, ogen uses that field to decide the variant.
    3. Field-based discrimination: If no explicit discriminator is provided, ogen infers it from the structure of the variants:
      • Field name discrimination: Variants have different required field names.
      • Field type discrimination: Variants have fields with the same name but different types (e.g., {id: string} vs {id: integer}). ogen checks the JSON type of the field at runtime.
      • Field value discrimination: Variants have fields with the same name and type, but different enum values. The enum values must be disjoint (non-overlapping). If they overlap, ogen will error and suggest an explicit discriminator.
  4. How SSE Event Shapes Work

    main

    Ogen supports multiple ways to represent Server-Sent Events (SSE) in OpenAPI via the x-ogen-sse-event-shape extension. This is necessary because there is no single standard for SSE in OAS 3.2.

    • data-only (Default): The schema describes only the data field. Standard SSE fields (id, event, retry) are parsed automatically by the client.
    • full: The schema describes the entire SSE event envelope (including event, id, etc.). This is useful when you need to use a discriminator on the event field.
    • full-array: The array form of the full shape, where the schema is an array of full SSE event envelopes.
    # Example of 'full' shape
    text/event-stream:
      x-ogen-sse-event-shape: full
      schema:
        oneOf:
          - $ref: "#/components/schemas/EventA"
          - $ref: "#/components/schemas/EventB"
        discriminator:
          propertyName: event
          mapping:
            event_a: "#/components/schemas/EventA"
            event_b: "#/components/schemas/EventB"
    
    # ...
    
    EventA:
      type: object
      required: [ event, data ]
      properties:
        event:
          type: string
          enum: [ event_a ]
        data:
          $ref: "#/components/schemas/EventAData"
  5. Using const values in OpenAPI schemas

    main

    The ogen generator supports the JSON Schema const keyword. When a field is marked with const, the value is hardcoded into the generated JSON encoder. This means you do not need to manually set these fields when initializing your Go structs.

    Example Schema

    components:
      schemas:
        ErrorResponse:
          type: object
          properties:
            code:
              type: integer
              const: 400
            status:
              type: string
              const: "error"
            message:
              type: string

    Generated Behavior

    Even though the struct contains the fields, the encoder bypasses the struct values for const fields:

    type ErrorResponse struct {
        Code    int64  `json:"code"`    // const: 400
        Status  string `json:"status"`  // const: "error"
        Message string `json:"message"`
    }
    
    // The encoder implementation (simplified):
    func (s *ErrorResponse) encodeFields(e *jx.Encoder) {
        {
            e.FieldStart("code")
            e.Int64(400)  // Const value encoded directly
        }
        {
            e.FieldStart("status")
            e.Str("error")  // Const value encoded directly
        }
        {
            e.FieldStart("message")
            e.Str(s.Message)  // Regular field
        }
    }

    Benefits

    • Simplified initialization: No need to set const fields in code.
    • Performance: Values are encoded directly without runtime lookups.
    • Type safety: Validated at code generation time.
    type ErrorResponse struct {
        Code    int64  `json:"code"`    // const: 400
        Status  string `json:"status"`  // const: "error"
        Message string `json:"message"`
    }
    
    func (s *ErrorResponse) encodeFields(e *jx.Encoder) {
        {
            e.FieldStart("code")
            e.Int64(400)  // Const value encoded directly
        }
        {
            e.FieldStart("status")
            e.Str("error")  // Const value encoded directly
        }
        {
            e.FieldStart("message")
            e.Str(s.Message)  // Regular field
        }
    }
  6. How complex `uniqueItems` validation works in ogen

    main

    When a schema uses uniqueItems: true on an array of complex objects (structs), ogen enables validation by generating three interconnected components:

    1. Equal(b Type, depth int) bool method: Performs a deep equality check between two instances. It uses a depth parameter to track recursion and prevents infinite loops from circular references. If the depth exceeds the limit (default: 10), it panics with a DepthLimitError.
    2. Hash() uint64 method: Computes a fast hash using the FNV-1a algorithm. This allows for $O(n)$ duplicate detection. The implementation ensures that if a.Equal(b) is true, then a.Hash() == b.Hash().
    3. validateUnique[TypeName]() error function: The runtime validator that uses hash buckets to detect duplicates. It iterates through the array, computes hashes, and uses the Equal() method to resolve any hash collisions. If a duplicate is found, it returns a DuplicateItemsError containing the indices of the offending items.
    // Example of the generated Equal method pattern
    func (a WorkflowStatus) Equal(b WorkflowStatus, depth int) bool {
        if depth > 10 {
            panic(&validate.DepthLimitError{
                MaxDepth: 10,
                TypeName: "WorkflowStatus",
            })
        }
        // ... comparison logic
        return true
    }
  7. How sum types (oneOf) are implemented

    main

    When an OpenAPI schema uses oneOf, ogen generates a Go struct that acts as a sum type. This struct contains a discriminator field and fields for all possible variants.

    Example: Sum type for [string, integer]

    If an ID can be either a string or an integer, ogen generates:

    type ID struct {
    	Type   IDType
    	String string
    	Int    int
    }
    
    // Helpers
    func NewStringID(v string) ID
    func NewIntID(v int) ID
    type ID struct {
    	Type   IDType
    	String string
    	Int    int
    }
    
    // Also, some helpers:
    func NewStringID(v string) ID
    func NewIntID(v int) ID
  8. Organize Operations with Operation Groups

    main

    For large APIs, you can group operations using x-ogen-operation-group. This causes ogen to generate separate handler interfaces for each group. Un-grouped operations are collected into a single main Handler interface which embeds all group interfaces.

    paths:
      /images:
        x-ogen-operation-group: Images
        get:
          operationId: listImages
          ...
      /images/{imageID}:
        x-ogen-operation-group: Images
        get:
          operationId: getImageByID
          ...
      /users:
        x-ogen-operation-group: Users
        get:
          operationId: listUsers
          ...
    // x-ogen-operation-group: Images
    type ImagesHandler interface {
        ListImages(ctx context.Context, req *ListImagesRequest) (*ListImagesResponse, error)
        GetImageByID(ctx context.Context, req *GetImagesByIDRequest) (*GetImagesByIDResponse, error)
    }
    
    // x-ogen-operation-group: Users
    type UsersHandler interface {
        ListUsers(ctx context.Context, req *ListUsersRequest) (*ListUsersResponse, error)
    }
    
    type Handler interface {
        ImagesHandler
        UsersHandler
        // All un-grouped operations will be on this interface
    }
  9. Schema patterns for complex `uniqueItems`

    main

    To trigger the generation of Equal(), Hash(), and validateUnique() methods, define an array in your OpenAPI schema with uniqueItems: true containing complex object types.

    Supported field types within these objects include:

    • Primitives (string, number, integer, boolean)
    • Optional and Nullable fields
    • Enums
    • Arrays (including nested objects)
    • Maps (additionalProperties)
    • Nested objects
    WorkflowStatus:
      type: object
      required: [id, name]
      properties:
        id: {type: string}
        name: {type: string}
        description: {type: string}
        properties:
          $ref: '#/components/schemas/StatusProperties'
  10. Generate Go code from OpenAPI specification

    main

    You can use ogen to generate a statically typed client and server from an OpenAPI v3 JSON or YAML file.

    Using go generate

    Add a //go:generate directive to your Go files to integrate the generator into your build workflow:

    //go:generate go run github.com/ogen-go/ogen/cmd/ogen --target target/dir -package api --clean schema.json

    Using Docker

    Alternatively, run ogen inside a container to avoid local installation dependencies:

    docker run --rm \
      --volume ".:/workspace" \
      ghcr.io/ogen-go/ogen:latest --target workspace/petstore --clean workspace/petstore.yml

    Flags:

    • --target <dir>: The directory where the generated code will be placed.
    • --package <name>: The name of the Go package for the generated code.
    • --clean: Cleans the target directory before generation.
    • --clean <file>: (In Docker context) specifies the file to clean.
  11. Implement Custom Validation

    main

    You can define custom validation logic in your OpenAPI schema using x-ogen-validate. To make these work, you must register the corresponding validator in your Go code using the validate package before performing validation.

    components:
      schemas:
        Product:
          type: object
          properties:
            name:
              type: string
              x-ogen-validate:
                minWords: 2
            tags:
              type: array
              items:
                type: string
              x-ogen-validate:
                uniqueItems: true
            metadata:
              type: object
              additionalProperties: true
              x-ogen-validate:
                fieldCount:
                  min: 1
                  max: 10
    import "github.com/ogen-go/ogen/validate"
    
    // Register validators
    validate.RegisterValidator("minWords", func(value any, params any) error {
        // ... validate minimum word count
    })
    validate.RegisterValidator("uniqueItems", func(value any, params any) error {
        // ... validate array has no duplicate items
    })
    validate.RegisterValidator("fieldCount", func(value any, params any) error {
        // ... validate object field count within min/max range
    })