Common Lisp client library for multiple OpenWeatherMap APIs: https://openweathermap.org/api
Large portions of the codebase are AI-assisted. For more information checkout AGENTS.md and /plans.
Active development. Targeting Quicklisp inclusion.
- Idiomatic Common Lisp client interface.
- Strong unit test suite.
- Separate integration test suite for live API checks.
- Project-local OpenAPI specifications per API family.
Set your API key:
export OPENWEATHER_API_KEY="your-key"Load the library via Quicklisp:
(ql:quickload :openweathermap)Configure API key and make a first request:
(openweathermap:configure-api-key (uiop:getenv "OPENWEATHER_API_KEY"))
(let ((current (openweathermap:fetch-current-weather :q "London" :units :metric)))
(format t "~&City: ~A~%" (openweathermap:ht-get current "name"))
(format t "Temp (C): ~F~%" (openweathermap:ht-get current "main" "temp")))All JSON fetch responses are decoded to hash-tables with string keys (via com.inuoe.jzon). Use ht-get to read fields: (openweathermap:ht-get result "name").
- SBCL
- ASDF
- Quicklisp (for external dependencies such as FiveAM)
- Node.js (required for OpenAPI linting with Redocly CLI)
- Recommended: Node >= 20.19.0 (or >= 22.12.0)
- npm
npm installThis installs @redocly/cli for OpenAPI spec validation.
make unitLoad the main library system:
make replLoad test systems in REPL:
make repl-tests
make repl-integrationSet your API key and enable live integration runs:
export OPENWEATHER_API_KEY="your-key"
export OPENWEATHERMAP_RUN_LIVE_TESTS=1
make integrationPer-API integration smoke targets:
make integration-onecall
make integration-current
make integration-forecast
make integration-geocoding
make integration-air-pollution
make integration-mapsIf OPENWEATHERMAP_RUN_LIVE_TESTS is not set to 1 (or =true/=yes), integration tests are skipped.
make spec-checkmake update-weather-conditionsmake examplesWhen OPENWEATHER_API_KEY is set, live examples are included. Without it, only offline examples run.
Set OPENWEATHERMAP_RUN_LIVE_EXAMPLES=1 to require live example execution.
make clean-cache- Use
make checkfor local quality checks. - Use
make unitormake checkfor deterministic non-live checks (CI-safe default). - Use
make update-weather-conditionsto refresh generated weather condition/icon data files from OpenWeather docs. - Keep implementation in
src/. - Keep unit tests in
tests/. - Keep live API integration tests in
integration-tests/. - Keep plans in
plans/. - Keep API specs in
spec/(one file per API family). - Update
CHANGELOG.mdfor each session.
- Module layout by API family:
src/apis/current.lispsrc/apis/forecast.lispsrc/apis/geocoding.lispsrc/apis/air-pollution.lispsrc/apis/maps.lispsrc/client.lispkeeps shared request/retry/decode pipeline.
- Function naming:
build-*functions generate endpoint URLs.make-*functions return request metadata plist(:method :get :url "...").fetch-*functions execute HTTP and return decoded hash-table responses (or raw tile payload for maps).src/apis/onecall.lispkeeps One Call endpoints (fetch-onecall,fetch-timemachine, etc.).
- Response strategy:
- JSON objects decode to hash-tables with string keys (via
com.inuoe.jzon). - JSON arrays decode to vectors.
- Use
ht-getaccessors to read response fields.
- JSON objects decode to hash-tables with string keys (via
Common conditions and how to inspect them:
missing-api-key-error: API key not configured.invalid-parameters-error: local contract validation failure.api-request-error: non-200 API response.api-network-error: transport/network failure after retries.api-response-parse-error: response decode failure.
Reader accessors available for structured handling:
api-request-error-status-codeapi-request-error-messageapi-request-error-endpointinvalid-parameters-error-message
Example:
(handler-case
(openweathermap:fetch-current-weather :q "London" :units :metric)
(openweathermap:invalid-parameters-error (err)
(format t "~&Invalid params: ~A~%"
(openweathermap:invalid-parameters-error-message err)))
(openweathermap:api-request-error (err)
(format t "~&API error status=~A endpoint=~A message=~A~%"
(openweathermap:api-request-error-status-code err)
(openweathermap:api-request-error-endpoint err)
(openweathermap:api-request-error-message err))))- JSON response objects are hash-tables with string keys (via
com.inuoe.jzon).- Example: use
(openweathermap:ht-get result "name")to read a top-level field. - Nested access:
(openweathermap:ht-get result "main" "temp"). - JSON arrays are vectors; use
areforloop for item across result.
- Example: use
- Hash-table accessor helpers:
ht-get,ht-p,ht-keys,ht-values,ht-map,ht->alist,ht->alist*. - Query parameter rest arguments must be even key/value lists.
- Example invalid form:
:unitswith missing value.
- Example invalid form:
- One Call family URL builders validate required positional arguments locally:
build-onecall-urlrequires numericlat/lonbuild-timemachine-urlrequires integerdtbuild-day-summary-urlrequiresYYYY-MM-DDdate
- Current/Forecast fetch helpers decode JSON only; use
:mode "json"or omit:mode. - Current/Forecast location selectors are exclusive:
- exactly one selector:
lat/lonpair,q,id, orzip.
- exactly one selector:
Configure client settings:
(openweathermap:configure-api-key "your-api-key")
(openweathermap:configure-client
:request-timeout-seconds 10
:max-retries 2
:retry-backoff-seconds 1)Fetch JSON-decoded responses (hash-table form):
(openweathermap:fetch-current-weather :q "London" :units :metric)
(openweathermap:fetch-forecast :q "London" :units :metric :cnt 8)
(openweathermap:fetch-geocoding "London" :limit 5)
(openweathermap:fetch-reverse-geocoding 35.0 139.0 :limit 1)
(openweathermap:fetch-zip-geocoding "94040" :country-code "US")
(openweathermap:fetch-air-pollution 35.0 139.0)
(openweathermap:fetch-air-pollution-forecast 35.0 139.0)
(openweathermap:fetch-air-pollution-history 35.0 139.0 1700000000 1700003600)
(openweathermap:fetch-weather-tile :temp_new 3 4 5 :opacity 0.7)
(openweathermap:fetch-onecall 35.0 139.0 :units :metric :lang "en")
(openweathermap:fetch-timemachine 35.0 139.0 1700000000 :units :metric)
(openweathermap:fetch-day-summary 35.0 139.0 "2026-02-15" :units :metric)
(openweathermap:fetch-overview 35.0 139.0 :date "2026-02-16")Build request URLs only (no network call):
(openweathermap:build-current-weather-url :lat 35.0 :lon 139.0 :units :metric)
(openweathermap:build-forecast-url :lat 35.0 :lon 139.0 :units :metric :cnt 8)
(openweathermap:build-geocoding-url "London" :limit 5)
(openweathermap:build-reverse-geocoding-url 35.0 139.0 :limit 1)
(openweathermap:build-zip-geocoding-url "94040" :country-code "US")
(openweathermap:build-air-pollution-url 35.0 139.0)
(openweathermap:build-air-pollution-forecast-url 35.0 139.0)
(openweathermap:build-air-pollution-history-url 35.0 139.0 1700000000 1700003600)
(openweathermap:build-weather-tile-url :temp_new 3 4 5 :opacity 0.7)
(openweathermap:build-onecall-url 35.0 139.0 :units :metric)
(openweathermap:build-timemachine-url 35.0 139.0 1700000000)
(openweathermap:build-day-summary-url 35.0 139.0 "2026-02-15")
(openweathermap:build-overview-url 35.0 139.0)Hash-table accessor helpers:
;; Read response fields
(openweathermap:ht-get result "name") ; top-level field
(openweathermap:ht-get result "main" "temp") ; nested access
(openweathermap:ht-p result) ; is it a hash-table?
(openweathermap:ht-keys result) ; list of string keys
(openweathermap:ht->alist result) ; shallow alist conversion
(openweathermap:ht->alist* result) ; recursive alist conversionWeather condition helpers:
(openweathermap:lookup-weather-condition 500)
(openweathermap:weather-icon-url "10d") ; => .../10d@2x.png
(openweathermap:weather-icon-url "10d" :size :1x) ; => .../10d.png
(openweathermap:resolve-weather-condition
:id 500 :icon "10n" :description "light rain" :main "Rain")
;; enrich-weather-entry accepts hash-table (from fetch-*) or plist
(openweathermap:enrich-weather-entry weather-entry-hash-table)| Function | Signature | Returns | Notes |
|---|---|---|---|
ht-get | (hash-table &rest keys) | value or NIL | Read nested value by successive string keys. Returns NIL on missing key. |
ht-p | (value) | boolean | True when VALUE is a hash-table. |
ht-keys | (hash-table) | list | List of string keys in HASH-TABLE. |
ht-values | (hash-table) | list | List of values in HASH-TABLE. |
ht-map | (function hash-table) | list | Apply FUNCTION to each (key value) pair, collecting results. |
ht->alist | (hash-table) | alist | Shallow conversion to association list. |
ht->alist* | (hash-table) | alist | Recursive conversion; nested hash-tables and vectors become nested alists/lists. |
| Function | Signature | Returns | Notes |
|---|---|---|---|
lookup-weather-condition | (condition-id) | plist or NIL | Looks up canonical condition by numeric ID (e.g. 500). |
weather-icon-url | (icon-code &key size secure-p) | string URL | Icon code must be NNd or NNn. size accepts :1x, :2x (default), :4x. |
resolve-weather-condition | (&key id icon description main) | plist | Merges payload fields with canonical mapping; includes fallback metadata and :unknown-condition-p. |
enrich-weather-entry | (weather-entry) | plist | Enriches one weather object (hash-table from fetch-* or plist) with canonical condition metadata. |
enrich-weather-list | (weather-list) | list | Maps enrichment across list or vector of weather objects. |
Resolution behavior:
- Payload
:descriptionand:iconare preferred when present. - Canonical mapping is used as fallback and for metadata like
:icon-day/:icon-night. - Unknown condition IDs do not error; result marks
:unknown-condition-pas true.
Runnable helper demos:
examples/07-weather-condition-helpers.lisp(offline)examples/08-current-weather-enrichment.lisp(live current-weather enrichment)examples/09-live-parser-contract-smoke.lisp(live parser/contract smoke across JSON APIs)- See
examples/README.orgfor commands.
This client accepts Common Lisp values and serializes them into query params.
| Param | Type | Allowed / Format | Example | Used By |
|---|---|---|---|---|
:units | symbol/string | :standard, :metric, :imperial | :units :metric | One Call, Current, Forecast |
:lang | string | language code | :lang "en" | One Call, Current, Forecast |
:mode | string | API-supported response mode | :mode "json" | Current, Forecast |
Notes:
fetch-current-weatherandfetch-forecastcurrently support only JSON-decoded responses; use:mode "json"(or omit:mode).- Non-JSON modes (for example
xml=/=html) can still be generated viabuild-*andmake-*helpers.
| Endpoint Family | Function(s) | Param | Type | Required | Notes / Example |
|---|---|---|---|---|---|
| One Call | fetch-onecall / build-onecall-url | lat, lon | number | yes | positional args |
| One Call | fetch-onecall / build-onecall-url | :exclude | string | no | comma-separated, e.g. "minutely,alerts" |
| Timemachine | fetch-timemachine | dt | integer | yes | UNIX timestamp |
| Day Summary | fetch-day-summary | date | string | yes | YYYY-MM-DD |
| Day Summary | fetch-day-summary | :tz | string | no | timezone offset/name per API docs |
| Overview | fetch-overview | :date | string | no | YYYY-MM-DD |
| Current/Forecast | fetch-current-weather, fetch-forecast | location selector | see note | yes | exactly one selector: :lat/:lon pair, :q, :id, or :zip |
| Forecast | fetch-forecast | :cnt | integer | no | number of forecast entries (1..40) |
| Geocoding | fetch-geocoding | query (positional) | non-empty string | yes | city/place query |
| Geocoding | fetch-geocoding | :limit | integer | no | max results (1..5) |
| Reverse Geo | fetch-reverse-geocoding | lat, lon | number | yes | positional args |
| Reverse Geo | fetch-reverse-geocoding | :limit | integer | no | max results (1..5) |
| ZIP Geo | fetch-zip-geocoding | zip (positional) | non-empty string | yes | e.g. "94040" |
| ZIP Geo | fetch-zip-geocoding | :country-code | string | no | e.g. "US" |
| Air Pollution | fetch-air-pollution, ...-forecast | lat, lon | number | yes | positional args |
| Air History | fetch-air-pollution-history | start, end | integer | yes | UNIX timestamps, start < end= |
| Maps | fetch-weather-tile | layer | keyword/string/symbol | yes | one of :clouds_new, :precipitation_new, :pressure_new, :temp_new, :temperature_new, :wind_new |
| Maps | fetch-weather-tile | z, x, y | non-negative integer | yes | tile coordinates |
| Maps | fetch-weather-tile | :opacity | number | no | e.g. 0.7 |
| Maps | fetch-weather-tile | :palette | string | no | e.g. "warm/cold" |
The table below summarizes successful (200 OK) response shapes based on OpenWeather docs and current project specs.
| API Family / Endpoint | Function(s) | Response Type | Top-level fields (typical) |
|---|---|---|---|
One Call /data/3.0/onecall | fetch-onecall | JSON object (hash-table) | lat number, lon number, timezone string, timezone_offset integer, current object, minutely array, hourly array, daily array, alerts array |
One Call Timemachine /data/3.0/onecall/timemachine | fetch-timemachine | JSON object (hash-table) | lat, lon, timezone, timezone_offset, data array of weather objects |
One Call Day Summary /data/3.0/onecall/day_summary | fetch-day-summary | JSON object (hash-table) | lat, lon, tz, date (YYYY-MM-DD), units, temperature object, humidity object, cloud_cover object, pressure object, precipitation object, wind object |
One Call Overview /data/3.0/onecall/overview | fetch-overview | JSON object (hash-table) | lat, lon, tz, date, units, weather_overview string |
Current Weather /data/2.5/weather | fetch-current-weather | JSON object (hash-table) | coord object, weather array, base string, main object, visibility integer, wind object, clouds object, rain object?, snow object?, dt integer, sys object, timezone integer, id integer, name string, cod integer/string |
Forecast /data/2.5/forecast | fetch-forecast | JSON object (hash-table) | cod string, message number, cnt integer, list array (forecast steps), city object |
Geocoding Direct /geo/1.0/direct | fetch-geocoding | JSON array (vector) | each item typically has name string, local_names object?, lat number, lon number, country string, state string? |
Geocoding Reverse /geo/1.0/reverse | fetch-reverse-geocoding | JSON array (vector) | same item shape as direct geocoding |
Geocoding ZIP /geo/1.0/zip | fetch-zip-geocoding | JSON object (hash-table) | zip string, name string, lat number, lon number, country string |
Air Pollution Current /data/2.5/air_pollution | fetch-air-pollution | JSON object (hash-table) | coord object, list array; each item includes dt, main (aqi), components (pollutant concentrations) |
Air Pollution Forecast /data/2.5/air_pollution/forecast | fetch-air-pollution-forecast | JSON object (hash-table) | same top-level shape as current air pollution |
Air Pollution History /data/2.5/air_pollution/history | fetch-air-pollution-history | JSON object (hash-table) | same top-level shape as current air pollution |
Maps Tile /map/{layer}/{z}/{x}/{y}.png | fetch-weather-tile | Binary payload | PNG image bytes/string payload (not JSON) |
Notes:
- JSON objects decode to hash-tables with string keys (use
ht-get result "name", notgetfwith keywords). - JSON arrays decode to vectors (use
areforloop for item across result). - Optional fields may be absent depending on endpoint, location, and data availability.
- For One Call endpoint schemas, see
spec/onecall.yaml.
src/
tests/
integration-tests/
spec/
examples/
plans/