libopenapi

repository·main·Indexed 21 days ago

https://github.com/pb33f/libopenapi

An enterprise-grade Go toolkit for handling complex OpenAPI 3, 3.1, and 3.2 specifications. It supports advanced features including Overlays, Arazzo, bundling, and model mutation. The library provides capabilities to generate Go models from OpenAPI schemas and convert Go reflection types into OpenAPI schemas, with support for polymorphism (oneOf), custom scalar aliases, and detailed generator diagnostics.

Tokens
6.3K
Snippets
23
Records
28
Agent score
75%

What's inside libopenapi

  1. Configure metadata for Go models

    main

    Metadata can be provided to the generator via several layers:

    1. Field Tags: Use openapi:"..." tags on Go struct fields (e.g., openapi:"format=uuid;readOnly;minLength=3").
    2. External Registry Overrides: Use WithTypeSchema, WithFieldSchema, or WithFieldSchemaByJSONName during generator initialization.
    3. Type-level Providers: Implement OpenAPISchema() *base.SchemaProxy on your types.

    Key Configuration Options:

    • WithOpenAPITags(true): Includes compact openapi tags in generated Go models for metadata that reflection cannot infer (e.g., format, title, description, enum, const).
    • WithSchemaMetadataSidecar(true): Generates a schema_metadata.go file containing typed Go data via OpenAPISchemaMetadata() any. This allows high-fidelity reflection without requiring the model package to import libopenapi or carry escaped YAML strings.
    gen := golang.NewGenerator(
        golang.WithFieldSchema(reflect.TypeOf(BookingPayment{}), "Source", sourceSchema),
        golang.WithFieldSchemaByJSONName(reflect.TypeOf(BookingPayment{}), "status", statusSchema),
    )
  2. Handle polymorphism (oneOf) in Go to OpenAPI

    main

    To render OpenAPI oneOf as a typed union in Go, the schema must have an explicit discriminator or share an inferable const discriminator property.

    When generating OpenAPI from Go reflection, you must register interface variants and their discriminator mapping using WithOneOfTypes and WithDiscriminatorMapping.

    gen := golang.NewGenerator(
        golang.WithOneOfTypes((*PaymentMethod)(nil), Card{}, Bank{}),
        golang.WithDiscriminatorMapping((*PaymentMethod)(nil), "object", map[string]string{
            "card": "#/components/schemas/Card",
            "bank": "#/components/schemas/Bank",
        }),
    )
  3. Quick-start tutorial: Parse an OpenAPI file using Go

    main

    You can quickly start using libopenapi to parse OpenAPI specifications by following these steps:

    1. Download a sample specification (e.g., the Petstore YAML):

      curl https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/_archive_/schemas/v3.0/pass/petstore.yaml > petstorev3.json
    2. Install the library:

      go get github.com/pb33f/libopenapi
    3. Parse the document using libopenapi.NewDocument and build a versioned model (e.g., BuildV3Model()).

    Note: When iterating over components, be aware that certain traversal methods might have specific behaviors regarding repeated access or iteration limits.

    package main
    
    import (
    	"fmt"
    	"os"
    	"github.com/pb33f/libopenapi"
    )
    
    func main() {
    	petstore, _ := os.ReadFile("petstorev3.json")
    	document, err := libopenapi.NewDocument(petstore)
    	if err != nil {
    		panic(fmt.Sprintf("cannot create new document: %e", err))
    	}
    	docModel, err := document.BuildV3Model()
    	if err != nil {
    		panic(fmt.Sprintf("cannot create v3 model from document: %e", err))
    	}
    
    	// Iterate through schemas in the components section
    	for schemaName, schema := range docModel.Model.Components.Schemas.FromOldest() {
    		if schema.Schema().Properties != nil {
    			fmt.Printf("Schema '%s' has %d properties\n", schemaName, schema.Schema().Properties.Len())
    		}
    	}
    }
  4. How high-level (porcelain) and low-level (plumbing) APIs work together

    main

    The libopenapi library uses a dual-layer approach to handle OpenAPI specifications:

    1. High-level (Porcelain) API: Provides strongly typed, easy-to-use models (e.g., v3high.Document) for navigating and manipulating the specification. This is what most developers use for business logic.
    2. Low-level (Plumbing) API: Provides access to the raw data, including comments, line numbers, and column numbers. This is essential for tools that need to perform precise edits or maintain formatting.

    Every high-level type provides a GoLow() method. Calling this allows you to 'drop down' from the high-level model to the low-level data for more granular operations. When you use RenderAndReload(), the library uses the high-level model to generate new bytes and then re-parses them to rebuild the low-level index, ensuring that line/column information remains accurate even after mutations.

  5. Configure additionalProperties behavior

    main

    Schema-valued additionalProperties renders as an AdditionalProperties map[string]T field in Go with the json:"-" tag. Generated objects receive MarshalJSON and UnmarshalJSON methods to ensure unknown properties are captured in the map.

    Use WithAdditionalPropertiesMethods(false) if you want the struct field but prefer to provide your own JSON marshaling logic.

  6. Configure naming conventions for generated models

    main

    The generator handles common Go initialisms (e.g., ID, URL, UUID) automatically.

    Customization Options:

    • Nesting Delimiters: Use WithNestedTypeNameDelimiter(string) to change the default _ delimiter for nested types (e.g., Order_PaymentSource). Passing an empty string produces compact names like OrderPaymentSource.
    • Collision Resolution: Colliding OpenAPI keys (e.g., user-id and user_id) are resolved using a double underscore suffix (e.g., UserID__2).
    • Custom Resolvers: Use WithTypeNameResolver, WithFieldNameResolver, WithEnumValueNameResolver, or WithNameResolver for project-specific naming requirements.
  7. Create an OpenAPI Document

    main

    To work with an OpenAPI or Swagger specification, you must first create a Document instance. You can do this by passing a []byte array containing the specification (either JSON or YAML) to NewDocument or NewDocumentWithConfiguration.

    Note that NewDocument does not automatically follow file or remote references. If you need to control how references are resolved (e.g., allowing or disallowing local/remote files), use NewDocumentWithConfiguration and provide a *datamodel.DocumentConfiguration.

    import "github.com/pb33f/libopenapi"
    
    // Basic usage
    doc, err := libopenapi.NewDocument(specBytes)
    if err != nil {
        // handle error
    }
    
    // Usage with configuration for references
    config := &datamodel.DocumentConfiguration{
        AllowFileReferences: true,
        AllowRemoteReferences: false,
    }
    doc, err = libopenapi.NewDocumentWithConfiguration(specBytes, config)
  8. Configure Go to OpenAPI generation with options

    main

    When generating OpenAPI schemas from Go types, use slice-based variants for package-level helpers that require configuration options.

    Example of using SchemasFromTypesWithOptions to register interface variants for polymorphism:

    set, err := golang.SchemasFromTypesWithOptions(
        []reflect.Type{reflect.TypeOf(Customer{})},
        golang.WithOneOfTypes((*PaymentMethod)(nil), Card{}, Bank{}),
    )
  9. Customize scalar aliases in Go to OpenAPI generation

    main

    You can map custom scalar aliases to specific OpenAPI schemas without adding methods to the Go types by using golang.NewGenerator with WithTypeSchema.

    gen := golang.NewGenerator(
        golang.WithTypeSchema(reflect.TypeOf(CustomerID("")), customerIDSchema),
    )
  10. Generate Go models from OpenAPI schemas

    main

    Use the golang package to convert OpenAPI schema/component models into Go source code. You can generate a single schema using RenderSchema or an ordered map of components using Generator.RenderSchemas.

    RenderSchemas returns a *GeneratedFile containing:

    • PackageName: The name of the generated package.
    • Source: The gofmt-formatted Go source code.
    • SchemaMetadata: An optional schema_metadata.go sidecar source (if enabled).
    • Types: Top-level generated type names and kinds.
    • Diagnostics: Notable generator decisions or limitations.
    source, err := golang.RenderSchema("Pet", schemaProxy)
    if err != nil {
        return err
    }
    fmt.Println(string(source))
  11. Generate OpenAPI schemas from Go types

    main

    Convert Go reflection types into OpenAPI schema/component models.

    • Use SchemaFromType for a single schema.
    • Use SchemasFromTypes to create a reusable component graph.

    SchemaSet.Root contains the first requested root. SchemaSet.Components contains named structs, registered interface unions, and reusable model shapes. Nested named model references are rendered as #/components/schemas/... refs.

    Note on Nullability: The generator uses JSON Schema 2020-12 native nullability (type: [T, "null"]) rather than the OpenAPI 3.0 nullable: true keyword.

    set, err := golang.SchemasFromTypes(reflect.TypeOf(Customer{}))
    if err != nil {
        return err
    }
    root := set.Root
    components := set.Components
  12. Understand generator diagnostics

    main

    Diagnostics are not validation errors; they report lossy model-shape choices, unsupported keywords, or naming collisions. Diagnostics include a stable Code, a Path, and a Message. Callers should branch on the Code rather than the message text.

    Common Diagnostic Codes:

    • DiagnosticComponentNameCollision: Colliding component names.
    • DiagnosticExternalReference: Use of external $ref values.
    • DiagnosticFieldNameCollision: Colliding field names.
    • DiagnosticPatternProperties: patternProperties do not map cleanly to Go fields.
    • DiagnosticTypeNameCollision: Colliding type names.
    • DiagnosticValidationKeyword: Unsupported validation-only keywords.