OpenAPI Linter

Saturday, Nov 1, 2025| Tags: perl, OpenAPI

DISCLAIMER: Image is generated using ChatGPT.


  1. Introduction

  2. What is OpenAPI::Linter?

  3. Installation

  4. Basic Usage

  5. Validating OpenAPI Specifications

  6. Finding Linting Issues

  7. Advanced Filtering

  8. Real-World Example

  9. Command Line Tool

10. Supported OpenAPI Versions


Introduction


OpenAPI has become the standard for designing, building and documenting RESTful APIs. However, ensuring your OpenAPI specifications are both syntactically correct and follow best practices can be challenging. OpenAPI::Linter brings comprehensive validation and linting capabilities directly into your Perl applications and development workflow, helping you catch issues before they impact your API consumers.


What is OpenAPI::Linter?


OpenAPI::Linter is a dual-purpose tool that provides both structural validation against the official OpenAPI JSON Schema and custom linting for best practices. It automatically detects your OpenAPI version and applies the appropriate validation rules, supporting versions 3.0.x through 3.1.x.


Installation


Install from CPAN:


$ cpanm -vS OpenAPI::Linter

The module has minimal dependencies and will automatically install JSON::Validator for schema validation and YAML::XS for YAML file parsing.


Sample Spec File


File: api.yml

openapi: 3.0.3
info:
  title: Sample User API
  # version: 1.0.0  # INTENTIONALLY COMMENTED OUT to demonstrate error
  description: A sample API for user management
  # license:  # INTENTIONALLY MISSING to demonstrate warning
  contact:
    name: API Team
    email: api@example.com
servers:
  - url: https://api.example.com/v1
    description: Production server
paths:
  /users:
    get:
      summary: Get all users
      # description: Retrieve a list of all users  # INTENTIONALLY MISSING
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'
        '500':
          description: Internal server error
    post:
      summary: Create a new user
      description: Create a new user in the system
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
      responses:
        '201':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Bad request
  /users/{id}:
    get:
      summary: Get user by ID
      # description: Retrieve a specific user by their ID  # INTENTIONALLY MISSING
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found
    put:
      summary: Update user
      description: Update an existing user's information
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
      responses:
        '200':
          description: User updated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found
components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
          # description: User's unique identifier  # INTENTIONALLY MISSING
        name:
          type: string
          description: User's full name
        email:
          type: string
          format: email
          description: User's email address
        age:
          type: integer
          minimum: 0
          # description: User's age in years  # INTENTIONALLY MISSING
        status:
          type: string
          enum: [active, inactive, suspended]
          description: User account status
      required:
        - id
        - name
        - email
    Error:
      # type: object  # INTENTIONALLY MISSING to demonstrate warning
      properties:
        code:
          type: integer
          description: Error code
        message:
          type: string
          description: Error message
        details:
          type: string
          description: Additional error details
  responses:
    NotFound:
      description: The specified resource was not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: Internal server error
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

File: api-valid.yml

openapi: 3.1.0
info:
  title: Sample User API
  version: 1.0.0
  description: A sample API for user management
  license:
    name: MIT
    url: https://opensource.org/licenses/MIT
  contact:
    name: API Team
    email: api@example.com
    url: https://example.com/support
servers:
  - url: https://api.example.com/v1
    description: Production server
  - url: https://staging-api.example.com/v1
    description: Staging server
paths:
  /users:
    get:
      summary: Get all users
      description: Retrieve a paginated list of all users in the system
      parameters:
        - name: page
          in: query
          description: Page number for pagination
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          description: Number of items per page
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  users:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  pagination:
                    $ref: '#/components/schemas/Pagination'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      summary: Create a new user
      description: Create a new user in the system with the provided information
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserInput'
      responses:
        '201':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '400':
          description: Bad request - invalid input data
        '500':
          $ref: '#/components/responses/ServerError'
  /users/{id}:
    get:
      summary: Get user by ID
      description: Retrieve a specific user by their unique identifier
      parameters:
        - name: id
          in: path
          required: true
          description: User's unique identifier
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/ServerError'
components:
  schemas:
    User:
      type: object
      description: A user object containing all user information
      properties:
        id:
          type: string
          format: uuid
          description: User's unique identifier
        name:
          type: string
          description: User's full name
        email:
          type: string
          format: email
          description: User's email address
        age:
          type: integer
          minimum: 0
          maximum: 150
          description: User's age in years
        status:
          type: string
          enum: [active, inactive, suspended]
          description: User account status
        createdAt:
          type: string
          format: date-time
          description: When the user was created
        updatedAt:
          type: string
          format: date-time
          description: When the user was last updated
      required:
        - id
        - name
        - email
        - status
    UserInput:
      type: object
      description: Input data for creating or updating a user
      properties:
        name:
          type: string
          description: User's full name
        email:
          type: string
          format: email
          description: User's email address
        age:
          type: integer
          minimum: 0
          maximum: 150
          description: User's age in years
      required:
        - name
        - email
    Pagination:
      type: object
      description: Pagination metadata
      properties:
        page:
          type: integer
          description: Current page number
        limit:
          type: integer
          description: Number of items per page
        total:
          type: integer
          description: Total number of items
        pages:
          type: integer
          description: Total number of pages
    Error:
      type: object
      description: Standard error response
      properties:
        code:
          type: integer
          description: HTTP status code
        message:
          type: string
          description: Error message
        details:
          type: string
          description: Additional error details
        timestamp:
          type: string
          format: date-time
          description: When the error occurred
      required:
        - code
        - message
        - timestamp
  responses:
    NotFound:
      description: The specified resource was not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    ServerError:
      description: Internal server error occurred
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

Basic Usage


Let’s start with a simple validation example.


File: validate.pl

use OpenAPI::Linter;

my $linter = OpenAPI::Linter->new(spec => $ARGV[0]);

my @schema_errors = $linter->validate_schema;

if (@schema_errors) {
    print "Schema validation failed:\n";
    foreach my $error (@schema_errors) {
        print $linter->format_schema_error($error->{message}) . "\n";
    }
} else {
    print "Schema validation passed!\n";
}

my @linting_issues = $linter->find_issues;

if (@linting_issues) {
    print "\nLinting issues found:\n";
    foreach my $issue (@linting_issues) {
        print "  [$issue->{level}] $issue->{message}\n";
    }
} else {
    print "No linting issues found!\n";
}

Output


$ perl validate.pl api.yml
Schema validation failed:
  - /info/version: Missing property.
  - /paths/users~001{id}/get/parameters/0/$ref: /oneOf/1 Missing property.
  - /paths/users~001{id}/put/parameters/0/$ref: /oneOf/1 Missing property.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/1 Not in enum list: query.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/2 Not in enum list: header.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/3 Not in enum list: cookie.
  - /paths/users~1{id}/get/parameters/0/required:
      /oneOf/0/allOf/2/oneOf/0 Not in enum list: true.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/1 Not in enum list: query.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/2 Not in enum list: header.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/3 Not in enum list: cookie.
  - /paths/users~1{id}/put/parameters/0/required:
      /oneOf/0/allOf/2/oneOf/0 Not in enum list: true.

Linting issues found:
  [ERROR] Missing info.version
  [WARN] Missing info.license
  [WARN] Missing description for get /users
  [WARN] Missing description for get /users/{id}
  [WARN] Schema Error missing type
  [WARN] Schema User.age missing description
  [WARN] Schema User.id missing description

$ perl validate.pl api-valid.yml
Schema validation passed!
No linting issues found!

Validating OpenAPI Specifications


validate_schema performs structural validation against the official OpenAPI JSON Schema. This ensures your specification conforms to the OpenAPI standard.


File: schema-validation.pl

use OpenAPI::Linter;

my $linter = OpenAPI::Linter->new(spec => {
    openapi => '3.0.0',
    info => {
        title   => 'My API',
        version => '1.0.0'
    },
    paths => {}
});

my $errors = $linter->validate_schema;

if (@$errors) {
    die "Invalid OpenAPI specification:\n" . join("\n", @$errors);
}

print "Specification is structurally valid!\n";

Output


$ perl schema-validation.pl
Specification is structurally valid!

Finding Linting Issues


find_issues performs best-practice checks beyond basic schema validation. It identifies common issues that can affect API usability and documentation quality.


File: linting.pl

use OpenAPI::Linter;

my $spec = {
    openapi => '3.0.0',
    info => {
        title   => 'My API',
        version => '1.0.0'
        # Missing description and license
    },
    paths => {
        '/users' => {
            'get' => {
                responses => {
                    '200' => {
                        description => 'OK'
                    }
                }
                # Missing operation description
            }
        }
    }
};

my $linter = OpenAPI::Linter->new(spec => $spec);
my @issues = $linter->find_issues;

foreach my $issue (@issues) {
    printf "[%-5s] %s\n", $issue->{level}, $issue->{message};
}

Output


$ perl linting.pl
[WARN ] Missing info.description
[WARN ] Missing info.license
[WARN ] Missing description for get /users

Advanced Filtering


find_issues supports filtering by severity level and message patterns, making it easy to focus on specific types of issues.

File: filtering.pl

use OpenAPI::Linter;

my $linter = OpenAPI::Linter->new(spec => $ARGV[0]);

# Get only errors (no warnings)
my @errors = $linter->find_issues(level => 'ERROR');

# Get warnings related to descriptions
my @description_warnings = $linter->find_issues(
    level => 'WARN',
    pattern => qr/description/i
);

# Get path-related issues
my @path_issues = $linter->find_issues(
    pattern => qr/^Missing description for \w+/
);

print "Critical errors: " . scalar(@errors) . "\n";
print "Description warnings: " . scalar(@description_warnings) . "\n";
print "Path issues: " . scalar(@path_issues) . "\n";

Output


$ perl filtering.pl api.yml
Critical errors: 1
Description warnings: 4
Path issues: 2

Real-World Example


Here’s a complete example that validates a realistic API specification and handles both schema and linting issues.


File: complete-validation.pl

use JSON;
use OpenAPI::Linter;

eval {
    my $linter = OpenAPI::Linter->new(spec => $ARGV[0]);

    my @schema_errors = $linter->validate_schema;

    if (@schema_errors) {
        print "Schema validation failed:\n";
        foreach my $error (@schema_errors) {
            print $linter->format_schema_error($error->{message}) . "\n";
        }
        exit 1;
    }

    print "Schema validation passed\n";

    my @issues = $linter->find_issues;

    my ($errors, $warnings) = (0, 0);
    foreach my $issue (@issues) {
        if ($issue->{level} eq 'ERROR') {
            $errors++;
            print "$issue->{message}\n";
        } else {
            $warnings++;
            print "$issue->{message}\n";
        }
    }

    if ($errors) {
        print "\n$errors error(s), $warnings warning(s) found - please fix errors\n";
        exit 1;
    } elsif ($warnings) {
        print "\n$warnings warning(s) found - consider addressing these\n";
        exit 0;
    } else {
        print "No issues found! Specification looks great!\n";
        exit 0;
    }
};

if ($@) {
    die "Failed to process $spec_file: $@\n";
}

Output


$ perl complete-validation.pl api.yml
Schema validation failed:
  - /info/version: Missing property.
  - /paths/users~001{id}/get/parameters/0/$ref: /oneOf/1 Missing property.
  - /paths/users~001{id}/put/parameters/0/$ref: /oneOf/1 Missing property.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/1 Not in enum list: query.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/2 Not in enum list: header.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/3 Not in enum list: cookie.
  - /paths/users~1{id}/get/parameters/0/required:
      /oneOf/0/allOf/2/oneOf/0 Not in enum list: true.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/1 Not in enum list: query.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/2 Not in enum list: header.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/3 Not in enum list: cookie.
  - /paths/users~1{id}/put/parameters/0/required:
      /oneOf/0/allOf/2/oneOf/0 Not in enum list: true.

Command Line Tool


OpenAPI::Linter comes with a convenient command-line tool openapi-linter for quick validation during development and CI/CD pipelines.


Basic Linting

$ openapi-linter --spec api.yml
[ERROR] Missing info.version
[WARN] Missing info.license
[WARN] Missing description for get /users
[WARN] Missing description for get /users/{id}
[WARN] Schema User.age missing description
[WARN] Schema User.id missing description
[WARN] Schema Error missing type

Summary: 1 ERROR, 6 WARNs

Schema Validation Mode

$ openapi-linter --spec api.yml --validate
Running schema validation for api.yml...
  - /info/version: Missing property.
  - /paths/users~001{id}/get/parameters/0/$ref: /oneOf/1 Missing property.
  - /paths/users~001{id}/put/parameters/0/$ref: /oneOf/1 Missing property.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/1 Not in enum list: query.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/2 Not in enum list: header.
  - /paths/users~1{id}/get/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/3 Not in enum list: cookie.
  - /paths/users~1{id}/get/parameters/0/required:
      /oneOf/0/allOf/2/oneOf/0 Not in enum list: true.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/1 Not in enum list: query.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/2 Not in enum list: header.
  - /paths/users~1{id}/put/parameters/0/in:
      /oneOf/0/allOf/2/oneOf/3 Not in enum list: cookie.
  - /paths/users~1{id}/put/parameters/0/required:
      /oneOf/0/allOf/2/oneOf/0 Not in enum list: true.

Summary: 11 ERRORs, 0 WARNs

JSON Output for Automation

$ openapi-linter --spec api.yml --json
{
   "issues" : [
      {
         "level" : "ERROR",
         "message" : "Missing info.version"
      },
      {
         "message" : "Missing info.license",
         "level" : "WARN"
      },
      {
         "message" : "Missing description for get /users/{id}",
         "level" : "WARN"
      },
      {
         "level" : "WARN",
         "message" : "Missing description for get /users"
      },
      {
         "message" : "Schema User.id missing description",
         "level" : "WARN"
      },
      {
         "level" : "WARN",
         "message" : "Schema User.age missing description"
      },
      {
         "message" : "Schema Error missing type",
         "level" : "WARN"
      }
   ],
   "summary" : {
      "warnings" : 6,
      "errors" : 1
   }
}

Supported OpenAPI Versions


OpenAPI::Linter automatically detects and supports:


OpenAPI 3.0.x: 3.0.0, 3.0.1, 3.0.2, 3.0.3
OpenAPI 3.1.x: 3.1.0, 3.1.1

The version is auto-detected from the openapi field, but you can explicitly specify it:


my $linter = OpenAPI::Linter->new(
    spec    => $spec,
    version => '3.1.0'  # Override auto-detection
);

OpenAPI::Linter helps you maintain high-quality API specifications by catching issues early in the development process. Whether you’re using it in your CI/CD pipeline, as a pre-commit hook or integrated directly into your application, it ensures your OpenAPI specifications are both valid and follow best practices.



Happy Hacking !!!

SO WHAT DO YOU THINK ?

If you have any suggestions or ideas then please do share with us.

Contact with me