Tested 2026-11-14. Disclosure: I work on UnAIMyText, which is why it's #3 in the tool list. The Go program is free to copy.

Our public API had 140 operations and about 40 of them had no summary at all. Someone on the platform team pointed Grok at the spec, one operation at a time, and a week later every gap was filled. The docs portal looked complete.

Then a customer emailed support to ask what "Seamlessly retrieve comprehensive invoice information." actually returned. Fair question. The sidebar showed that summary, period and all, for a plain GET /invoices/{id}. The description under it began "This endpoint allows users to effortlessly access invoice data," which tells you nothing the URL didn't.

So I wrote a linter. Give it a spec and it checks every operation. Give it --draft with an operation and some notes, and it asks xAI for a summary and description, then checks those before anyone pastes them in.

The xAI part, briefly

xAI's Responses API reference describes a plain HTTP call: POST https://api.x.ai/v1/responses with Authorization: Bearer $XAI_API_KEY and a JSON body holding model and input. The docs' examples use grok-4.7, so that's the default here, and XAI_MODEL overrides it. input can be a list of {role, content} messages, so the system prompt goes first. The reply's output is an array that can hold reasoning items as well as the message, so the program only reads items whose type is message and joins the output_text parts. I also send "store": false, since the docs say responses are otherwise kept for 30 days.

A fake key got me a 400 from the live endpoint, with "Incorrect API key provided." With no header it returned 401 and "No credentials presented." Both come out as exit 2.

What it checks

The OpenAPI spec defines summary as "a short summary of what the operation does" and description as a longer explanation where CommonMark is allowed. So markdown is fine in descriptions and flagged only in summaries, where most doc sidebars show it raw.

  • Missing summary
  • Summary over 60 characters (my cap; pick yours)
  • Summary ending with a period
  • Markdown in the summary
  • Description opening with "This endpoint" or "This API"
  • Description that just repeats the summary
  • Filler in any text, including parameter descriptions: seamless, robust, leverage, comprehensive, effortlessly, empower, streamline, allows users to
  • Emoji
  • TODO, TBD, lorem ipsum, [description] leftovers

It exits 0 when everything passes, 1 on lint findings and 2 for bad arguments, an unreadable spec, network trouble or API errors. It reads JSON only. If your spec is YAML, convert it first; the error message says so.

A spec it would flag

{ "paths": { "/invoices": { "get": { "summary": "Seamlessly retrieve a comprehensive list of all invoices in your account.", "description": "This endpoint allows users to effortlessly access invoice data. 🚀", "parameters": [{"name": "status", "in": "query", "description": "TODO"}] } } } } 

The program

// openapi_lint.go checks the summary and description text in an OpenAPI JSON spec, // or asks Grok (xAI Responses API) to draft them for one operation and checks the draft. // // Usage: // // go run openapi_lint.go openapi.json lint every operation // go run openapi_lint.go - read the spec from stdin // go run openapi_lint.go --draft "GET /invoices/{id}" "notes..." draft with xAI, then lint // add --json for machine-readable output // // Exit: 0 clean, 1 lint failures, 2 usage/network/API errors package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" "os" "regexp" "sort" "strings" "time" ) const summaryMax = 60 var ( methods = []string{"get", "put", "post", "delete", "options", "head", "patch", "trace"} filler = regexp.MustCompile(`(?i)\b(seamless(ly)?|robust|leverag(e|es|ing)|comprehensive|effortless(ly)?|empower(s|ing)?|streamlin(e|es|ing)|cutting-edge|allows users to|is designed to|with ease)\b`) restates = regexp.MustCompile(`(?i)^(this|the) (endpoint|api|operation|route)\b`) plainMarkup = regexp.MustCompile("\\*\\*|`|^#") emoji = regexp.MustCompile(`[\x{1F300}-\x{1FAFF}\x{2600}-\x{27BF}]`) placeholder = regexp.MustCompile(`(?i)\b(TODO|TBD|lorem ipsum)\b|\[(description|summary|insert[^\]]*)\]`) fence = regexp.MustCompile("(?s)^\\s*``` [a-z]*\\s*(.*?)\\s* ```\\s*$") ) type Op struct { Summary string `json:"summary"` Description string `json:"description"` Parameters []struct { Name string `json:"name"` Description string `json:"description"` } `json:"parameters"` } type Finding struct { Operation string `json:"operation"` Issue string `json:"issue"` } func lintOp(name string, op Op) []Finding { var out []Finding add := func(s string) { out = append(out, Finding{name, s}) } s, d := strings.TrimSpace(op.Summary), strings.TrimSpace(op.Description) if s == "" { add("missing summary") } else { if n := len([]rune(s)); n > summaryMax { add(fmt.Sprintf("summary too long (%d chars; cap %d)", n, summaryMax)) } if strings.HasSuffix(s, ".") { add("summary ends with a period") } if plainMarkup.MatchString(s) { add("markdown in summary (renders raw in most sidebars)") } } if restates.MatchString(d) { add(`description opens with "This endpoint..."; say what it returns or changes`) } if d != "" && strings.EqualFold(strings.TrimRight(d, "."), strings.TrimRight(s, ".")) { add("description repeats the summary") } texts := map[string]string{"summary": s, "description": d} for _, p := range op.Parameters { texts["parameter "+p.Name] = p.Description } keys := make([]string, 0, len(texts)) for k := range texts { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { t := texts[k] if m := filler.FindAllString(t, -1); len(m) > 0 { add(fmt.Sprintf("filler in %s: %s", k, strings.ToLower(strings.Join(m, ", ")))) } if emoji.MatchString(t) { add("emoji in " + k) } if placeholder.MatchString(t) { add("placeholder left in " + k) } } return out } func lintSpec(raw []byte) ([]Finding, int, error) { var spec struct { Paths map[string]map[string]json.RawMessage `json:"paths"` } if err := json.Unmarshal(raw, &spec); err != nil { return nil, 0, fmt.Errorf("not valid JSON (convert YAML first): %v", err) } if len(spec.Paths) == 0 { return nil, 0, fmt.Errorf("no paths found; is this an OpenAPI document?") } paths := make([]string, 0, len(spec.Paths)) for p := range spec.Paths { paths = append(paths, p) } sort.Strings(paths) var all []Finding count := 0 for _, p := range paths { for _, m := range methods { body, ok := spec.Paths[p][m] if !ok { continue } var op Op if err := json.Unmarshal(body, &op); err != nil { return nil, 0, fmt.Errorf("%s %s: %v", m, p, err) } count++ all = append(all, lintOp(strings.ToUpper(m)+" "+p, op)...) } } return all, count, nil } func draft(operation, notes string) (Op, error) { var op Op key := os.Getenv("XAI_API_KEY") if key == "" { return op, fmt.Errorf("XAI_API_KEY is not set") } base := strings.TrimRight(envOr("XAI_BASE_URL", "https://api.x.ai/v1"), "/") payload, _ := json.Marshal(map[string]any{ "model": envOr("XAI_MODEL", "grok-4.7"), "store": false, "input": []map[string]string{ {"role": "system", "content": "You write OpenAPI operation docs. Reply with JSON only: " + `{"summary": "...", "description": "..."}. Summary: imperative, under 60 characters, no period. ` + "Description: two or three plain sentences on what it returns or changes and the main error cases."}, {"role": "user", "content": operation + "\nNotes: " + notes}, }, }) req, _ := http.NewRequest("POST", base+"/responses", bytes.NewReader(payload)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+key) resp, err := (&http.Client{Timeout: 90 * time.Second}).Do(req) if err != nil { return op, fmt.Errorf("request failed: %v", err) } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode/100 != 2 { var e struct { Error string `json:"error"` } json.Unmarshal(body, &e) return op, fmt.Errorf("xAI returned %d: %s", resp.StatusCode, firstNonEmpty(e.Error, string(body))) } var r struct { Output []struct { Type string `json:"type"` Content []struct { Type string `json:"type"` Text string `json:"text"` } `json:"content"` } `json:"output"` } if err := json.Unmarshal(body, &r); err != nil { return op, fmt.Errorf("unexpected response: %v", err) } var text strings.Builder for _, item := range r.Output { if item.Type != "message" { continue // skip reasoning summaries and tool calls } for _, c := range item.Content { if c.Type == "output_text" { text.WriteString(c.Text) } } } t := strings.TrimSpace(text.String()) if m := fence.FindStringSubmatch(t); m != nil { t = m[1] // models like to wrap JSON in a code fence anyway } if t == "" { return op, fmt.Errorf("empty reply from model") } if err := json.Unmarshal([]byte(t), &op); err != nil { return op, fmt.Errorf("model did not return JSON: %.80s", t) } return op, nil } func envOr(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func firstNonEmpty(a, b string) string { if strings.TrimSpace(a) != "" { return a } return strings.TrimSpace(b) } func fail(msg string) { fmt.Fprintln(os.Stderr, "error:", msg) os.Exit(2) } func main() { args, asJSON := []string{}, false for _, a := range os.Args[1:] { if a == "--json" { asJSON = true } else { args = append(args, a) } } var findings []Finding count := 1 switch { case len(args) == 3 && args[0] == "--draft": parts := strings.Fields(args[1]) if len(parts) != 2 || !strings.HasPrefix(parts[1], "/") { fail(`operation must look like "GET /invoices/{id}"`) } op, err := draft(args[1], args[2]) if err != nil { fail(err.Error()) } shown, _ := json.MarshalIndent(op, "", " ") fmt.Fprintln(os.Stderr, string(shown)) findings = lintOp(strings.ToUpper(parts[0])+" "+parts[1], op) case len(args) == 1: var raw []byte var err error if args[0] == "-" { raw, err = io.ReadAll(os.Stdin) } else { raw, err = os.ReadFile(args[0]) } if err != nil { fail(err.Error()) } if findings, count, err = lintSpec(raw); err != nil { fail(err.Error()) } default: fail(`usage: openapi_lint [--json] SPEC.json | - | --draft "METHOD /path" "notes"`) } if asJSON { out, _ := json.MarshalIndent(map[string]any{"operations": count, "findings": findings}, "", " ") fmt.Println(string(out)) } else {Source:https://dev.to/unaimytext_app/humanize-xai-grok-output-in-go-an-openapi-description-linter-5-free-tools-2026-50bc

Related posts