NAME

OpenAPI::Linter - Validate and lint OpenAPI 3.x specification files

VERSION

Version 0.18

SYNOPSIS

use OpenAPI::Linter;

# Load from a YAML or JSON file
my $linter = OpenAPI::Linter->new(spec => 'openapi.yaml');

# Or pass a pre-parsed spec as a hashref
my $linter = OpenAPI::Linter->new(spec => \%spec_data);

# Run all checks (schema + semantic)
my @issues = $linter->find_issues;

# Run structural/schema checks only
my @errors = $linter->validate_schema;

# Filter by severity level
my @errors_only = $linter->find_issues(level => 'ERROR');

# Filter by message pattern
my @security = $linter->find_issues(pattern => qr/security/i);

# Inspect results
for my $issue (@issues) {
    my $loc = $issue->{location};

    # Location stringifies to the dot-separated spec path
    printf "[%s] %s (at %s)\n",
        $issue->{level},
        $issue->{message},
        $loc;

    # For file-based specs, precise line/column is also available
    if ($loc->line) {
        printf "    => %s\n", $loc->position;  # e.g. openapi.yaml:42:5
    }
}

DESCRIPTION

OpenAPI::Linter validates OpenAPI 3.0.x and 3.1.x specification files against both the official OpenAPI JSON Schema and a curated set of semantic and documentation rules.

Checks are organised into two phases:

1. Structural validation - the spec is validated against the official OpenAPI schema published at https://spec.openapis.org. If structural errors are found, further checks are skipped.
2. Semantic checks - a suite of opinionated rules is applied covering documentation completeness, naming conventions, security, HTTP method semantics, unused components, and more.

Results are returned as a list of issue hashrefs; see "ISSUE STRUCTURE" for the full field reference.

CONSTRUCTOR

new

my $linter = OpenAPI::Linter->new(%args);

Creates and returns a new linter instance.

Arguments

spec (required)

Either a filesystem path to a YAML or JSON OpenAPI file, or a hashref containing a pre-parsed specification. A path that does not exist causes a fatal error via croak.

schema_url (optional)

Override the OpenAPI meta-schema URL used for structural validation. By default the correct URL is chosen automatically based on the openapi version field in the spec:

  • 3.0.x - https://spec.openapis.org/oas/3.0/schema/2021-09-28

  • 3.1.x - https://spec.openapis.org/oas/3.1/schema/2022-10-07

Overriding this is primarily useful in tests or air-gapped environments.

Exceptions

new will croak if:

  • The spec argument is not provided.

  • spec is a string that does not refer to an existing file.

Parse errors (malformed YAML/JSON) and schema-download failures are captured and reported as ERROR-level issues on the first call to find_issues or validate_schema, rather than causing a fatal exception.

METHODS

validate_schema

my @issues = $linter->validate_schema;
my $issues = $linter->validate_schema;   # scalar context -> arrayref

Validates the spec against the official OpenAPI JSON Schema and checks the openapi version field. Returns a (possibly empty) list of issue hashrefs.

In scalar context returns an arrayref.

If a parse error was encountered during construction it is reported here and no further schema validation is attempted.

find_issues

my @issues = $linter->find_issues(%args);
my $issues = $linter->find_issues(%args);   # scalar context -> arrayref

Runs the full linting pipeline: structural validation first, then all semantic checks. If structural errors are found the semantic checks are skipped and only those errors are returned (after any filtering).

In scalar context returns an arrayref.

Optional arguments

level

Return only issues whose level field exactly matches the given string. Valid values are ERROR, WARN, and INFO.

pattern

A compiled or uncompiled regular expression. Only issues whose message field matches the pattern are returned.

Both filters may be combined; an issue must satisfy both to be included.

ISSUE STRUCTURE

Each issue is a plain hashref with the following keys:

level

Severity of the issue. One of:

  • ERROR - the spec is invalid or will cause interoperability failures.

  • WARN - a best-practice violation; the spec may still be functional.

  • INFO - informational; e.g. duplicate descriptions.

message

A human-readable description of the problem.

path

A dot-separated or #/-prefixed location within the spec indicating where the issue was found. May be absent for top-level parse errors.

type

A machine-readable category string. Common values:

documentation   Missing description, summary, example, or licence.
semantic        HTTP method misuse, missing operationId, etc.
schema          Violation of the OpenAPI JSON Schema.
syntax          YAML/JSON parse failure.
security        Missing or incomplete security definitions.
naming          Path segment or property naming convention violations.
validation      Server variable, version format, or other validation.
maintainability Unused components, duplicate descriptions, etc.
rule (optional)

A short rule identifier, present on selected checks (e.g. info-description, security-defined, no-undefined-server-variable).

CHECKS PERFORMED

Structural

  • openapi version field format must be X.Y.Z.

  • openapi version must be 3.0.x or 3.1.x.

  • Full validation against the official OpenAPI JSON Schema.

Info section

  • info.description is present.

  • info.license is present.

Operations (paths)

  • Every operation has a description, summary, and operationId.

  • GET and DELETE operations do not include a requestBody.

  • requestBody has a description.

  • Non-path parameters have a description.

  • Path parameters declare required: true.

  • Every operation has at least one 2xx response.

  • Every response object has a description.

  • Response schemas that include content have a type or $ref.

  • POST operations should return 201 Created.

  • 204 No Content responses must not include a body.

  • GET operations should not return 201.

Security

Checked only when the spec defines components.securitySchemes.

  • A root-level security field is present.

  • Each operation either has its own security field or inherits one from the root.

Servers

  • Every {variable} placeholder in a server URL is declared in servers[n].variables.

  • A server URL that consists entirely of a single bare placeholder (e.g. "{siteUrl}"), with no variables block, is downgraded to WARN rather than ERROR. This pattern is common in published specs to indicate that the consumer must supply the base URL. It is still reported so the spec author is aware, but it is not treated as a hard error. Any other URL that mixes literal text with an undefined variable (e.g. "https://{host}/api") remains an ERROR.

Components

  • Every schema in components.schemas has a description, type, and (for object schemas) an example or examples.

  • Every schema property has a description and uses camelCase naming.

  • Every entry in components.parameters has a description.

  • Every entry in components.responses has a description.

  • Every entry in components.requestBodies has a description.

  • Schemas, responses, and request bodies defined in components but never referenced via $ref are reported as unused.

Path naming

  • Static path segments must be kebab-case ([a-z][a-z0-9]*(-[a-z][a-z0-9]*)*).

Duplicate descriptions

  • Identical descriptions across multiple component schemas are flagged at INFO level.

ENVIRONMENT

DEBUG

Set to a true value to emit verbose diagnostic output to STDERR tracing the linter's internal execution.

DEBUG=1 perl myscript.pl
MOJO_CONNECT_TIMEOUT / MOJO_INACTIVITY_TIMEOUT

Temporarily set to 10 seconds during schema download to prevent indefinite hangs on network failures.

DEPENDENCIES

JSON::Validator
JSON
YAML::XS
File::Slurp
URI (optional - used for stricter uri-reference format validation when available)

DIAGNOSTICS

A 'spec' file path or data is required

You called new without supplying the spec argument.

Spec path '%s' does not exist

The string passed as spec is not a path to an existing file.

Parsing error: ...

The file could not be parsed as YAML or JSON. The underlying error from YAML::XS is included in the message.

BUGS AND LIMITATIONS

  • $ref resolution is shallow: only direct $ref usage in operation requestBody, responses, parameters, and response content schemas is tracked for the unused-components check. Nested or recursive $ref usage is not followed.

  • The unused-components check for components.parameters only reports a parameter named fragment (for Redocly toolchain compatibility). All other unused parameters are silently ignored.

  • Examples, headers, links, and callbacks inside components are not checked.

  • The schema is downloaded from spec.openapis.org on first use and cached by JSON::Validator. Subsequent instantiations reuse the cache. In environments without internet access, pass a local schema_url.

SEE ALSO

AUTHOR

Mohammad Sajid Anwar, <mohammad.anwar at yahoo.com>

REPOSITORY

https://github.com/manwar/OpenAPI-Linter

BUGS

Please report any bugs or feature requests through the web interface at https://github.com/manwar/OpenAPI-Linter/issues. I will be notified and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

You can find documentation for this module with the perldoc command.

perldoc OpenAPI::Linter

You can also look for information at:

LICENSE AND COPYRIGHT

Copyright (C) 2026 Mohammad Sajid Anwar.

This program is free software; you can redistribute it and / or modify it under the terms of the the Artistic License (2.0). You may obtain a copy of the full license at: http://www.perlfoundation.org/artistic_license_2_0 Any use, modification, and distribution of the Standard or Modified Versions is governed by this Artistic License.By using, modifying or distributing the Package, you accept this license. Do not use, modify, or distribute the Package, if you do not accept this license. If your Modified Version has been derived from a Modified Version made by someone other than you,you are nevertheless required to ensure that your Modified Version complies with the requirements of this license. This license does not grant you the right to use any trademark, service mark, tradename, or logo of the Copyright Holder. This license includes the non-exclusive, worldwide, free-of-charge patent license to make, have made, use, offer to sell, sell, import and otherwise transfer the Package with respect to any patent claims licensable by the Copyright Holder that are necessarily infringed by the Package. If you institute patent litigation (including a cross-claim or counterclaim) against any party alleging that the Package constitutes direct or contributory patent infringement,then this Artistic License to you shall terminate on the date that such litigation is filed. Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.