Skip to content

Support attributes (nullable, format, optional) and type placeholders in .swaggo overrides #2147

@isasmendiagus

Description

@isasmendiagus

Problem

The .swaggo override file lets users globally replace types (e.g., replace sql.NullString string), but falls short when the replacement needs additional schema metadata — specifically nullable, optional, and format properties.

Use case: three-state PATCH fields

In REST APIs, PATCH endpoints often need to distinguish three states per field:

State JSON Meaning
Absent field not in payload Don't change
Null "field": null Clear the value
Value "field": 42 Set the value

A common Go solution is a generic Optional[T] with custom UnmarshalJSON. For correct OpenAPI output, each field using this type should be optional (not in required) and nullable (x-nullable: true).

Today, users must annotate every field with struct tags:

type PatchUserRequest struct {
    Name Optional[string] `json:"name" validate:"optional" extensions:"x-nullable"`
    Bio  Optional[string] `json:"bio" validate:"optional" extensions:"x-nullable"`
}

This defeats the purpose of centralized overrides — the semantics are properties of the type, not individual fields.

No generic catch-all

Each Optional[T] instantiation must be listed individually:

replace common.Optional[string] string
replace common.Optional[int] integer
replace common.Optional[models.Address] models.Address
// ... one line per type

Related

This also addresses #1852 — supporting format in global overrides (e.g., date-time for time types).

Proposed Solution

1. Override attributes (key:value syntax)

Extend replace to accept key:value attributes after the replacement type:

replace common.Optional[string] string optional:true nullable:true
replace common.Optional[time.Time] string optional:true nullable:true format:date-time

Supported attributes:

  • optional:true — removes the field from required
  • nullable:true — adds x-nullable: true
  • format:<value> — sets format (e.g., date-time, uuid)

2. Type parameter placeholder ($T)

Allow $T in source and replacement types for generic catch-all rules:

replace common.Optional[$T] $T optional:true nullable:true

When swag encounters common.Optional[models.Address], it matches the pattern, captures models.Address, substitutes $T, resolves normally, and applies the attributes.

Exact matches take priority over placeholder patterns.

Complete .swaggo example

replace common.Optional[string] string optional:true nullable:true
replace common.Optional[float64] number optional:true nullable:true
replace common.Optional[time.Time] string optional:true nullable:true format:date-time
replace common.Optional[$T] $T optional:true nullable:true

Backward Compatibility

  • .swaggo format: Fully backward compatible — existing lines work unchanged.
  • SetOverrides API: Changes from map[string]string to map[string]Override. This is a breaking change for programmatic callers of SetOverrides, but this function is primarily used internally by the gen package.

Open question: attribute naming convention

The current implementation uses clean, short attribute names:

replace common.Optional[string] string optional:true nullable:true format:date-time

An alternative would be to mirror the existing struct tag names more closely:

replace common.Optional[string] string validate:optional extensions:x-nullable format:date-time

The struct-tag-aligned syntax would be more familiar to existing swag users (extensions:"x-nullable" and validate:"optional" are already well-known), but the shorter names are arguably cleaner for a config file context. Happy to adjust either way — would appreciate maintainer input on the preferred convention.

Implementation

I have a working implementation with tests ready as a PR: override struct, key:value parsing, $T matching, and attribute application. All existing tests pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions