Skip to content

Serve OpenAPI 3.0 spec at /openapi.v1.json#37038

Open
myers wants to merge 34 commits intogo-gitea:mainfrom
myers:feat/openapi3-conversion
Open

Serve OpenAPI 3.0 spec at /openapi.v1.json#37038
myers wants to merge 34 commits intogo-gitea:mainfrom
myers:feat/openapi3-conversion

Conversation

@myers
Copy link
Copy Markdown
Contributor

@myers myers commented Mar 30, 2026

Add a build-time conversion step that transforms the existing Swagger 2.0 spec into an OpenAPI 3.0 spec. The OAS3 spec is served alongside the existing Swagger 2.0 spec, enabling API clients that require OAS3 (progenitor, openapi-python-client, etc.) to generate code directly from Gitea's API.

New files:

  • build/generate-openapi.go — converts swagger v1_json.tmpl to OAS3
  • build/openapi3-tools.go — tool dependency for kin-openapi
  • routers/web/swagger_json.go — serves the OAS3 spec
  • templates/swagger/v1_openapi3_json.tmpl — generated OAS3 spec

This is not to be an answer to how gitea handles OAS3 long term, but a way to use what we have to move a step forward.

See https://github.com/myers/gt for how this can be used.

Written with Claude Code using Opus, and human reviewed.

@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Mar 30, 2026
@wxiaoguang
Copy link
Copy Markdown
Contributor

Can you add more comments and document more details in the tool's code? For example: why it needs knownEnumTypes mapping? etc.

Comment thread Makefile
@silverwind
Copy link
Copy Markdown
Member

silverwind commented Mar 30, 2026

I think we should probably also evaluate using OAS3 for swagger-ui. Long-term I would like us to move completely to a OAS3-based solution, ideally OAS 3.1.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an OpenAPI 3.0 (OAS3) JSON spec endpoint generated at build-time by converting the existing Swagger 2.0 template, so API clients that require OAS3 can generate SDKs directly while keeping the existing Swagger v1 spec.

Changes:

  • Serve an OAS3 spec at /openapi.v1.json alongside the existing /swagger.v1.json.
  • Add a generator (build/generate-openapi.go) to convert templates/swagger/v1_json.tmpltemplates/swagger/v1_openapi3_json.tmpl.
  • Wire generation + consistency checks into make generate-swagger / backend checks and add required Go module dependencies.

Reviewed changes

Copilot reviewed 7 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
routers/web/web.go Registers the new /openapi.v1.json route when Swagger is enabled.
routers/web/swagger_json.go Adds the OpenAPI3Json handler to render the OAS3 JSON template.
build/generate-openapi.go Implements Swagger2→OAS3 conversion and post-processing/enrichment steps.
build/openapi3-tools.go Attempts to pin kin-openapi tool deps for generation.
templates/swagger/v1_openapi3_json.tmpl Generated OAS3 JSON template served by the new route.
Makefile Adds OpenAPI3 generation/check targets and hooks into existing checks.
go.mod / go.sum Adds kin-openapi and related transitive deps.
.editorconfig Adds formatting rules for the generated OAS3 template.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Makefile Outdated
Comment thread build/generate-openapi.go Outdated
Comment thread build/openapi3-tools.go
@TheFox0x7
Copy link
Copy Markdown
Contributor

I think we should probably also evaluate using OAS3 for swagger-ui. Long-term I would like us to move completely to a OAS3-based solution, ideally OAS 3.1.

IMO this should be one step. Displaying spec that's converted from generated one does not feel right to me. It's a step too much in the pipeline and I'd prefer to be closer to what we directly control. I'll try to get back to generating API directly from structs after I'll finish dealing with my package related PRs as I'm tied up there a bit.

@silverwind
Copy link
Copy Markdown
Member

BTW I would make it just openapi.json, there will never be a v2, the naming of the swagger file is legacy but should be kept.

@TheFox0x7
Copy link
Copy Markdown
Contributor

what about v4 which is supposedly in works?

@silverwind
Copy link
Copy Markdown
Member

silverwind commented Mar 30, 2026

No idea about that , but I don't think anyone wants to maintain multiple versions of the api existing at the same time in the same repo, that's more like wishful thinking.

@silverwind
Copy link
Copy Markdown
Member

But I guess keep the v1 for consistency with swagger and the unlikely event that there will be v2+.

@silverwind
Copy link
Copy Markdown
Member

Regarding knownEnumTypes two better ideas:

  1. Create a small AST walker to extract the enums
  2. Leave the enums as-is, e.g. inline

@myers
Copy link
Copy Markdown
Contributor Author

myers commented Apr 11, 2026

Regarding knownEnumTypes two better ideas:

1. Create a small AST walker to extract the enums

2. Leave the enums as-is, e.g. inline

SDK generators like progenitor (Rust) need named enum types to produce good code. Without them, you get duplicate anonymous string types on every field instead of a shared StateType enum. Deduplicating enums and giving them proper names in #/components/schemas/ is the main motivation for the converter.

AST Walker: Walk Go source to find type StateType string declarations and extract the real type names. This would eliminate the map entirely and be correct by construction. But it's a significant scope increase — needs to parse Go source files, resolve which types are actually used in swagger-annotated structs, and handle edge cases.

Worth doing eventually but feels like a separate PR.

Or maybe better to focus efforts on building a better go annotations to OpenAPI 3 spec.

My goal here was the shortest path to being able to use OpenAPI ecosystem tools.

@myers
Copy link
Copy Markdown
Contributor Author

myers commented Apr 11, 2026

IMO this should be one step. Displaying spec that's converted from generated one does not feel right to me. I

I understand the concern — a two-stage pipeline (annotations → Swagger 2.0 → OAS3) is more steps than ideal. But I think this PR is a useful incremental step:

  1. It helps now — SDK generators and tooling that require OAS3 can consume Gitea's API today, without waiting for a better tool.
  2. It doesn't block the direct approach — when someone builds direct OAS3 generation from structs, this converter simply gets deleted. No tech debt accumulates because the converter is a standalone build tool with no runtime footprint
  3. The conversion is build-time only — the generated spec is committed as a template, so there's no runtime overhead or dependency on kin-openapi in the Gitea binary

That said, direct OAS3 generation would be strictly better long-term.

@myers myers force-pushed the feat/openapi3-conversion branch from e6f384f to e9cd8cf Compare April 11, 2026 17:43
@myers
Copy link
Copy Markdown
Contributor Author

myers commented Apr 11, 2026

@silverwind @TheFox0x7 I made some changes. Could we move forward with this?

If we don't want to move forward with this can you give me a path to get an openapi 3.x spec that you do like?

@TheFox0x7
Copy link
Copy Markdown
Contributor

Could we move forward with this?

Have you addressed reviews?

@myers
Copy link
Copy Markdown
Contributor Author

myers commented Apr 12, 2026

Could we move forward with this?

Have you addressed reviews?

I had missed some, thank you for the prompt.

@silverwind
Copy link
Copy Markdown
Member

silverwind commented Apr 17, 2026

Reviewed with Claude Opus 4.7 and opencode; aggregated findings below.

High

H1. Missing reserved username entryrouters/web/web.go:1746 + models/user/user.go:586-623

swagger.v1.json is in reservedUsernames at line 619; openapi.v1.json is not. Since /{username} is a catch-all, a user registered with name openapi.v1.json would shadow the new route. Add the string to reservedUsernames (and consider extending the TestRenameReservedUsername cases at tests/integration/user_test.go:172).

Medium

M1. addDeprecatedFlags substring matchbuild/generate-openapi.go:193

strings.Contains(desc, "deprecated") will false-positive on phrases like "not deprecated" or "previously deprecated, now supported". A word-boundary regex or a stricter check would be safer. Current Gitea descriptions appear safe, but the rule is fragile for future additions.

M2. fixFileSchemas only traverses one levelbuild/generate-openapi.go:105-129

Walks response/request-body media-types but doesn't recurse into schema.Properties, schema.Items, allOf/oneOf. Current output contains zero "type": "file" so the PR is fine today — just worth a recursive walker if this stays long-term.

M3. knownEnumTypes is a hand-maintained allowlistbuild/generate-openapi.go:307-314

Already discussed — author chose this over an AST walker as the shorter path. Worth documenting that this map must be updated when new enum types are added, otherwise new enums will silently get derived generic names. openapi3-check in CI will catch drift, so acceptable as-is.

Low

L1. Makefile prereq omits tools fileMakefile:263

$(OPENAPI3_SPEC): $(SWAGGER_SPEC) build/generate-openapi.go — if build/openapi3-tools.go changes (dep versions), regeneration isn't triggered. Cosmetic.

L2. package build + //go:build toolsbuild/openapi3-tools.go:6

All other files in build/ are package main with //go:build ignore. Works fine (author verified tidy-check passes on Go ≥1.17), just an outlier package layout.

L3. Naming inconsistencyMakefile variables/targets vs. URL path

URL /openapi.v1.json has no 3; template filename v1_openapi3_json.tmpl and Makefile vars/targets do. Pick one — minor.

L4. deriveEnumName fallback appends "Enum"build/generate-openapi.go:373

When no x-go-enum-desc extension exists, schemas get generic names like StatusEnum. Benign; just flagging so reviewers understand when a generic name appears.


Posted by Claude Opus 4.7 on behalf of @silverwind.

@silverwind
Copy link
Copy Markdown
Member

Regarding knownEnumTypes I'm still not quite happy. Ideas from Claude:

Better designs, ranked:

  1. Annotate the Go enum types themselves (e.g. // swagger:enum-type StateType comment → emitted as x-go-enum-type extension) so this converter reads the type name directly. Kills the map, zero future maintenance. Needs swagger-gen support though.
  2. AST walker — correct by construction but ~150 LOC and a separate Go-parsing pass. Bigger change for marginal gain.
  3. Fail loudly on unknown prefixes in strict mode, so CI breaks instead of silently producing FooEnum.

myers added 5 commits April 19, 2026 15:58
Add a build-time conversion step that transforms the existing Swagger 2.0
spec into an OpenAPI 3.0 spec. The OAS3 spec is served alongside the
existing Swagger 2.0 spec, enabling API clients that require OAS3
(progenitor, openapi-python-client, etc.) to generate code directly
from Gitea's API.

New files:
- build/generate-openapi.go — converts swagger v1_json.tmpl to OAS3
- build/openapi3-tools.go — tool dependency for kin-openapi
- routers/web/swagger_json.go — serves the OAS3 spec
- templates/swagger/v1_openapi3_json.tmpl — generated OAS3 spec
- Add build/generate-openapi.go as Makefile prerequisite so changes
  to the converter trigger spec regeneration
- Remove leftover `_ = key` no-op statement
- Add top-of-file comment explaining what the converter does and why
- Document why knownEnumTypes mapping is needed

Co-Authored-By: Claude Opus 4 (claude-opus-4-6)
The enumGroups loop declared key but never used it; use _ instead
so the generator compiles. Regenerate v1_openapi3_json.tmpl.

Co-Authored-By: Claude (Opus)
Introduces build/openapi3gen as the testable home for OAS3 converter
logic. Starts with EnumKey, the canonical value-set hash shared by
the enum scanner and the converter.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
ScanSwaggerEnumTypes reads .go files under each directory, finds
types documented with // swagger:enum TypeName, and returns a map
from canonical value-set key to type name. Happy-path only; failure
modes covered in follow-up commits.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
myers added 7 commits April 19, 2026 18:29
Task 8 promoted Permission fields to named string types
(RepoWritePermission for input, AccessLevelName for output). Update
integration tests that were building *string or comparing against
raw string literals to use the new typed constants.

Also fix latent runtime type mismatches in api_repo_collaborator_test.go
and api_repo_teams_test.go where assert.Equal was comparing string
literals against AccessLevelName fields (reflect.DeepEqual is
type-sensitive so these would have failed at runtime).

Co-Authored-By: Claude Sonnet 4.6 (claude-sonnet-4-6)
build/openapi3gen/convert.go is a normal Go package (not //go:build
ignore like generate-openapi.go) and is subject to the depguard rule
that forbids encoding/json in favor of modules/json.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
The new positive-assertion target should run in the same pipeline as
the diff-based swagger-check and openapi3-check. Without this, the
schema-name regression gate is runnable only manually.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
The kin-openapi dependency introduced by the OAS3 converter (and its
transitive deps) needed license entries. Regenerated after merging
upstream main, which pulled in tidy-check updates from other PRs.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
Replaces IIFE closures with Go 1.26's new(value) form, matching the
idiom used for other *string fields in the same file.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
The converter itself aborts on any unmapped shared enum (via
deriveEnumName's error path), and the existing openapi3-check
idempotency target catches drift in the generated spec. The
positive schema-name assertion was belt-and-suspenders with a
hardcoded enum list that would drift as new types are added.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
@myers
Copy link
Copy Markdown
Contributor Author

myers commented Apr 19, 2026

@silverwind

Regarding knownEnumTypes I'm still not quite happy. Ideas from Claude:
2. AST walker — correct by construction but ~150 LOC and a separate Go-parsing pass. Bigger change for marginal gain.

I pick this option. I ended up refactoring some other list of strings that should be enums.

What do you think?

EDIT: also addressed your CR findings.

myers added 3 commits April 19, 2026 19:59
Addresses H1 from review: the /openapi.v1.json route is shadowable
by a user registered with that name, since it's not in
reservedUsernames. Mirrors the existing entry for swagger.v1.json.
Test extended to cover both.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
Addresses M1 and L1 from review:

- addDeprecatedFlags now matches "deprecated" as a leading marker
  rather than any substring, preventing future false-positives on
  phrases like "not deprecated". Current output is unchanged.
- $(OPENAPI3_SPEC) rule now depends on build/openapi3-tools.go so
  changes to kin-openapi version pins trigger regeneration.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
Addresses L3 from review: the URL path /openapi.v1.json was
inconsistent with the template filename v1_openapi3_json.tmpl and
Makefile variables OPENAPI3_SPEC / generate-openapi3 etc. The "3"
indicates OpenAPI version 3.0.

Reserved username and integration test updated to match.

Co-Authored-By: Claude Opus 4.7 (claude-opus-4-7)
@silverwind
Copy link
Copy Markdown
Member

Re-reviewed with Claude Opus 4.7 after latest commits. The rename to /openapi3.v1.json, tighter deprecated-matcher, Makefile dep additions, and reserved-name update cover H1, M1, L1, and L3. Remaining items:

Medium

M2. fixFileSchemas single-level traversalbuild/openapi3gen/convert.go:51-85 (unchanged from prior review)

Walks only the top-level mediaType.Schema in responses/request bodies; no recursion into Properties, Items, or allOf. Zero "type": "file" in current output, so no-op today — but fragile if the annotation set ever adds a nested formData file.

Low

L2. build/openapi3-tools.go outlier layoutpackage build + //go:build tools (unchanged)

All other build/*.go files are package main + //go:build ignore. Works, but cosmetic outlier.

AST walker (enumscan.go, new findings)

A1. Single-pass ordering dependencybuild/openapi3gen/enumscan.go:127-151

collectEnumValues reads enumTypes[ident.Name] at the moment the CONST decl is processed. If a future commit ever places consts before their type's annotated decl, or splits type/consts across files where the consts' file sorts alphabetically first, values are silently dropped — then the final check throws a misleading "has no const block with typed string values". Fix: two-pass scan (collect all types first, then consts).

A2. Grouped type (...) blocks miss annotations on TypeSpecsbuild/openapi3gen/enumscan.go:105-125

collectEnumType only inspects gd.Doc; ts.Doc (per-spec doc inside a grouped type (...) decl) is ignored. No grouped decls exist in the annotated files today. Fix: also inspect each TypeSpec.Doc.

A3. Blank-line-separated comment groups — cosmetic

Only the CommentGroup adjacent to the decl becomes gd.Doc. An // swagger:enum Foo followed by a real blank line (not //) before the type is lost. Current code uses // blank separators so it stays in one group — safe in practice. Worth a comment in collectEnumType noting the caveat.


All three AST issues are latent: they would silently miss a type, but deriveEnumName fails loudly when a shared-enum schema has no AST match, so openapi3-check catches any real-world breakage.


Posted by Claude Opus 4.7 on behalf of @silverwind.

@silverwind
Copy link
Copy Markdown
Member

AST approach is fine, a few more points above then I can approve.

myers and others added 3 commits April 19, 2026 21:35
The exact-match reserved names openapi3.v1.json and swagger.v1.json are
caught by the reserved list before the *.keys-style pattern list, so
they produce name_reserved, not name_pattern_not_allowed. The previous
"contains dot" heuristic conflated the two. Split the fixtures into
reservedNames and patternNotAllowedNames, each with its own expected
message key, so both paths are covered explicitly.

Co-authored-by: Claude (Opus 4.7) <noreply@anthropic.com>
@silverwind
Copy link
Copy Markdown
Member

CI should pass now with the recent webkit removal from #37315.

Is all feedback addressed?

myers and others added 5 commits April 21, 2026 23:12
Top-level fixSchema walked only the root mediaType.Schema, leaving any
nested type: file schemas as invalid OAS3. Walk Properties, Items, and
AllOf/OneOf/AnyOf branches.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…yOf/not

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Previously collectEnumValues read enumTypes[ident.Name] at the moment a
CONST decl was visited. If consts were placed before their annotated
type — either within one file or across files sorted alphabetically —
the values were silently dropped. Collect every annotated type first,
then walk consts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
collectEnumType only looked at GenDecl.Doc, so annotations inside a
grouped type(...) block were skipped. Read each TypeSpec.Doc as well,
and note the blank-line caveat for future readers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@myers
Copy link
Copy Markdown
Contributor Author

myers commented Apr 21, 2026

Thanks for the careful re-review. Addressed M2/A1/A2/A3; declining L2 with reasoning below.

M2 (nested file-schema recursion) — fixed. fixSchema now walks Properties, Items, AllOf/OneOf/AnyOf, and Not. $ref nodes are skipped so a shared schema is still visited exactly once through its declaration. Added a unit test with a nested object + array items + allOf + oneOf + anyOf + not, all typed file, and asserts each is rewritten to string+binary. Regenerated spec has no diff — confirms we had zero nested cases today, which matches your observation.

L2 (build/openapi3-tools.go package layout) — declining, with reasoning. The file is a tools.go-style anchor whose sole purpose is to keep kin-openapi in go.mod so go mod tidy doesn't drop it. It has to be discoverable by tidy, which rules out //go:build ignore — that tag excludes the file from the dependency graph. //go:build tools + a named package is the canonical pattern for exactly this case (Go wiki: Modules — How can I track tool dependencies). Alternatives:

  1. Keep package build + //go:build tools (current).
  2. Move to build/tools/tools.go — still needs the same tag and a named package, just in a subdirectory.
  3. Blank-import from build/generate-openapi.go directly — that file is //go:build ignore, so tidy wouldn't see the imports and would drop the dep, so that doesn't work.

What do you suggest?

A1 (single-pass ordering dependency) — fixed. Two-pass scan: every annotated type is collected first across all files, then a second pass walks const decls. Added two tests: one with const(...) before type in the same file (would fail on the old code), and one with consts in a_consts.go and the annotated type in b_type.go to cover the cross-file ordering case.

A2 (grouped type(...) TypeSpec.Doc) — fixed. collectEnumType now checks both gd.Doc and each ts.Doc via a new registerEnumAnnotation helper. Added a test using a grouped type(...) block with per-spec annotations for two types.

A3 (blank-line comment-group caveat) — added a block comment on collectEnumType noting that Go only attaches a CommentGroup when it is directly adjacent to the decl; a real blank line drops the Doc. The rule is therefore "annotation must sit on the line directly above the type."


Drafted by Claude Opus 4.7

@myers
Copy link
Copy Markdown
Contributor Author

myers commented Apr 21, 2026

@silverwind

Is all feedback addressed?
yes

Resolved tests/integration/api_team_test.go: combined main's `apiTeam :=
DecodeJSON(t, resp, &api.Team{})` ergonomics with this branch's typed
api.AccessLevelName arguments to checkTeamResponse.

Regenerated templates/swagger/v1_openapi3_json.tmpl so the openapi3-check
backend check passes against the merged tree (picks up PreviousAttemptURL
on ActionRun and the new CreatePullReviewCommentReplyOptions schema).

Co-Authored-By: Claude <noreply@anthropic.com> (model: claude-opus-4-7)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants