API Changes and Evolution¶
Overview¶
The Danish Parliamentary Open Data API (oda.ft.dk) has evolved since its launch to provide stable, reliable access to parliamentary data. This document tracks the API's evolution, breaking changes, and provides guidance for developers adapting to changes.
Table of Contents¶
- Current API Version
- API Evolution History
- Breaking Changes
- Parameter and Query Changes
- Response Format Modifications
- Performance Improvements
- Error Handling Evolution
- Migration Guides
- Future Changes and Roadmap
- Change Impact Assessment
Current API Version¶
OData Version: 3.0
API Base URL: https://oda.ft.dk/api/
Status: Stable Production
Last Updated: 2025 (ongoing maintenance)
Version Strategy¶
- Single Version Implementation: No versioned endpoints (e.g., /v1/, /v2/)
- No Version Negotiation: OData 4.0 headers cause HTTP 404 responses
- Backwards Compatibility Focus: Changes maintain compatibility where possible
API Evolution History¶
Early Implementation (Pre-2020)¶
- Initial OData 3.0 implementation
- Basic entity exposure with standard CRUD operations
- Limited documentation and developer resources
Stability Period (2020-2023)¶
- Enhanced Performance: Query optimization and response time improvements
- Data Quality Improvements: Enhanced data validation and consistency
- Documentation Expansion: Comprehensive API documentation development
Recent Developments (2024-2025)¶
- Query Limit Adjustments: Pagination limits modified for performance optimization
- Enhanced Error Handling: More consistent HTTP status code responses
- Infrastructure Improvements: Server capacity and reliability enhancements
Breaking Changes¶
Critical Breaking Changes¶
1. Pagination Limit Reduction (2024-2025)¶
Change: Maximum records per request reduced from 1000 to 100
# Previous behavior (worked until 2024)
GET /api/Sag?%24top=1000 # Returned 1000 records
# Current behavior (2025)
GET /api/Sag?%24top=1000 # Returns only 100 records (hard limit)
Impact: High - affects all applications using large batch queries
Migration: Implement proper pagination with $skip parameter
Migration Code:
// Old approach (broken)
const response = await fetch('/api/Sag?%24top=1000');
// New approach (works)
async function fetchAllPages(entity, pageSize = 100) {
const results = [];
let skip = 0;
let hasMore = true;
while (hasMore) {
const url = `/api/${entity}?%24top=${pageSize}&%24skip=${skip}`;
const response = await fetch(url);
const data = await response.json();
results.push(...data.value);
hasMore = data.value.length === pageSize;
skip += pageSize;
}
return results;
}
2. OData 4.0 Header Rejection¶
Change: OData-Version 4.0 headers now cause HTTP 404 errors
# Fails with HTTP 404
curl -H "OData-Version: 4.0" "https://oda.ft.dk/api/Sag"
# Works correctly
curl "https://oda.ft.dk/api/Sag" # Uses OData 3.0 by default
Impact: Medium - affects clients explicitly requesting OData 4.0
Migration: Remove OData-Version headers or use version 3.0
Non-Breaking Changes¶
1. Enhanced Error Messages¶
- More descriptive HTTP 400 responses for invalid $expand parameters
- Consistent 404 responses for invalid entity names
- Improved error message clarity (no code changes required)
2. Performance Optimizations¶
- Server-side query optimization (transparent to clients)
- Improved response times for complex queries
- Enhanced caching for metadata endpoints
Parameter and Query Changes¶
URL Encoding Requirements (Ongoing)¶
Critical: OData parameters must use URL encoding
# Correct - URL encoded
GET /api/Sag?%24top=10&%24skip=20
# L Wrong - literal $ characters
GET /api/Sag?$top=10&$skip=20
# L Wrong - shell escaping
GET /api/Sag?\$top=10
Supported OData Parameters¶
Current Support (OData 3.0)¶
-
$expand- Entity relationship expansion (max 2 levels) -
$filter- Query filtering with string, date, and boolean operations -
$format- Response format (JSON default, XML available) -
$orderby- Result sorting -
$top- Result limit (max 100) -
$skip- Pagination offset -
$select- Field selection -
$inlinecount- Total count inclusion
Not Supported (OData 4.0+ Features)¶
- L
$search- Full-text search (use$filterwithsubstringof()) - L
$compute- Computed fields - L
$apply- Data aggregation - L
$batch- Batch operations
Filter Parameter Behavior¶
Silent Failure Pattern¶
Critical Caveat: Invalid filter field names are silently ignored
# Invalid field name - returns ALL records instead of error
GET /api/Sag?%24filter=nonexistentfield eq 'value'
# Valid filter - works correctly
GET /api/Sag?%24filter=titel eq 'Forslag til folketingsbeslutning'
Developer Impact: Typos in field names cause performance issues by returning unfiltered datasets
Response Format Modifications¶
JSON Response Structure (Stable)¶
The JSON response format has remained consistent:
{
"odata.metadata": "https://oda.ft.dk/api/$metadata#Sag",
"odata.count": "96538",
"value": [
{
"id": 12345,
"titel": "Example case",
"opdateringsdato": "2025-01-15T10:30:00"
}
]
}
XML Response Format (Stable)¶
XML format remains available but requires explicit parameter:
Content-Type Headers¶
- JSON:
application/json;odata=minimalmetadata(default) - XML:
application/atom+xml;type=feed(when requested)
Performance Improvements¶
Response Time Evolution¶
| Query Type | 2023 Performance | 2025 Performance | Improvement |
|---|---|---|---|
| Small queries (d10 records) | ~150ms | ~85-108ms | 28-40% faster |
| Medium queries (d100 records) | ~200ms | ~110-131ms | 35-45% faster |
| Large queries (d1000 records) | ~3000ms | ~2100ms | 30% faster |
Infrastructure Enhancements¶
- Server Optimization: Improved query processing algorithms
- Caching: Enhanced metadata and frequent query caching
- Network: CDN improvements for faster global access
- Database: Query index optimization for better performance
Concurrent Request Handling¶
- Rate Limiting: No rate limits detected in testing
- Concurrent Capacity: Successfully handles 10+ simultaneous requests
- Stability: No performance degradation under moderate load
Error Handling Evolution¶
Current Error Response Patterns¶
HTTP Status Codes¶
| Status | Condition | Response | Handling Strategy |
|---|---|---|---|
| 200 | Success | JSON/XML data | Process normally |
| 200 | Invalid filter field | All records returned | Validate field names |
| 400 | Invalid $expand | Error message | Check metadata for valid expansions |
| 404 | Invalid entity/ID | Empty response | Verify entity names and IDs |
| 500 | Server error | HTML error page | Retry request, contact support |
Error Handling Best Practices¶
async function robustApiCall(url) {
try {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 400) {
throw new Error('Invalid query parameters - check $expand and $filter syntax');
}
if (response.status === 404) {
throw new Error('Entity not found - verify endpoint name');
}
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// Check for silent filter failures (unexpectedly large datasets)
if (data.value.length > expected_max_size) {
console.warn('Received larger dataset than expected - check filter parameters');
}
return data;
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
Migration Guides¶
Migrating from Large Batch Queries¶
Problem¶
Applications using $top values > 100 now receive only 100 records.
Solution¶
Implement pagination with $skip parameter:
def fetch_all_records(entity_name, filters=None, batch_size=100):
"""
Fetch all records from an entity with automatic pagination
"""
base_url = "https://oda.ft.dk/api/"
all_records = []
skip = 0
while True:
# Build query URL
params = {
'$top': str(batch_size),
'$skip': str(skip)
}
if filters:
params['$filter'] = filters
# URL encode parameters
query_string = urllib.parse.urlencode(params)
url = f"{base_url}{entity_name}?{query_string}"
# Make request
response = requests.get(url)
response.raise_for_status()
data = response.json()
batch = data['value']
if not batch: # No more records
break
all_records.extend(batch)
skip += batch_size
# If we got fewer than batch_size records, we're done
if len(batch) < batch_size:
break
return all_records
Migrating from OData 4.0 Assumptions¶
Problem¶
Applications sending OData-Version 4.0 headers receive HTTP 404 errors.
Solution¶
Remove version headers or use OData 3.0:
// L Wrong - causes 404
fetch('/api/Sag', {
headers: {
'OData-Version': '4.0'
}
});
// Correct - no version header
fetch('/api/Sag');
// Also correct - explicit 3.0
fetch('/api/Sag', {
headers: {
'OData-Version': '3.0'
}
});
Handling Silent Filter Failures¶
Problem¶
Invalid field names in filters don't error - they return complete datasets.
Solution¶
Implement client-side validation:
// Valid field names for Sag entity (example subset)
const VALID_SAG_FIELDS = [
'id', 'titel', 'titelkort', 'offentlighedskode', 'nummer',
'nummerprefix', 'nummernumerisk', 'nummerpostfix', 'resume',
'afstemningskonklusion', 'periodeid', 'afgørelsesresultatkode',
'baggrundsmateriale', 'opdateringsdato', 'statsbudgetsag',
'begrundelse', 'paragrafnummer', 'paragraf', 'spørgsmålsordlyd',
'spørgsmålstitel', 'spørgsmålsid', 'procedurenummer', 'deltundersag'
];
function validateFilter(filterString, validFields) {
// Extract field names from filter (simple regex)
const fieldPattern = /(\w+)\s+(?:eq|ne|gt|lt|ge|le|startswith|endswith|substringof)/gi;
const matches = filterString.match(fieldPattern);
if (matches) {
for (const match of matches) {
const fieldName = match.split(/\s+/)[0];
if (!validFields.includes(fieldName)) {
throw new Error(`Invalid field name in filter: ${fieldName}`);
}
}
}
}
// Usage
try {
const filter = "titel eq 'Forslag til folketingsbeslutning'";
validateFilter(filter, VALID_SAG_FIELDS);
const response = await fetch(`/api/Sag?%24filter=${encodeURIComponent(filter)}`);
} catch (error) {
console.error('Filter validation failed:', error);
}
Future Changes and Roadmap¶
Planned Improvements (2025-2026)¶
1. Enhanced Documentation¶
- Interactive API Explorer: Web-based query builder and tester
- OpenAPI Specification: Modern API documentation format
- SDK Development: Official client libraries for popular languages
2. Performance Enhancements¶
- Caching Optimization: Improved response caching for frequent queries
- Query Optimization: Further database index improvements
- Response Compression: Automatic gzip compression for large responses
3. Developer Experience¶
- Better Error Messages: More descriptive error responses
- Field Validation: Server-side validation for filter parameters
- Query Suggestions: Helpful hints for invalid queries
Potential Breaking Changes (Under Consideration)¶
1. OData Version Upgrade¶
Timeline: No current plans
Impact: Would require significant compatibility updates
Status: Under evaluation for long-term roadmap
2. Authentication Introduction¶
Timeline: Not planned
Current Policy: API remains completely open
Monitoring: Usage patterns monitored for potential abuse
3. Rate Limiting Implementation¶
Timeline: If needed based on usage
Current Status: No rate limits in place
Trigger: Would only implement if service stability affected
Deprecated Features¶
Currently, no features are officially deprecated. The API maintains full backwards compatibility within the OData 3.0 specification.
Change Impact Assessment¶
Impact Classification System¶
Critical (=4)¶
- Changes that break existing applications
- Require immediate code updates
- Examples: Pagination limit reduction, parameter removal
Important (=á)¶
- Changes that affect performance or behavior
- Recommend code updates
- Examples: Error message improvements, performance optimizations
Minor (=â)¶
- Changes that improve experience without breaking functionality
- Optional code updates
- Examples: Documentation improvements, additional features
Testing Strategies for API Changes¶
1. Automated Testing¶
import unittest
import requests
class APICompatibilityTest(unittest.TestCase):
def setUp(self):
self.base_url = "https://oda.ft.dk/api/"
def test_basic_connectivity(self):
"""Test basic API connectivity"""
response = requests.get(f"{self.base_url}Sag?%24top=1")
self.assertEqual(response.status_code, 200)
def test_pagination_limits(self):
"""Test current pagination limits"""
response = requests.get(f"{self.base_url}Sag?%24top=100")
data = response.json()
self.assertLessEqual(len(data['value']), 100)
# Test over-limit request
response = requests.get(f"{self.base_url}Sag?%24top=1000")
data = response.json()
self.assertEqual(len(data['value']), 100) # Should cap at 100
def test_url_encoding_requirement(self):
"""Test that URL encoding is required"""
# This should work
encoded_url = f"{self.base_url}Sag?%24top=5"
response = requests.get(encoded_url)
self.assertEqual(response.status_code, 200)
def test_filter_validation(self):
"""Test filter parameter behavior"""
# Valid filter
valid_filter = "titel eq 'test'"
encoded_filter = requests.utils.quote(valid_filter)
response = requests.get(f"{self.base_url}Sag?%24filter={encoded_filter}")
self.assertEqual(response.status_code, 200)
# Invalid field name - should return 200 with all records (silent failure)
invalid_filter = "invalidfield eq 'test'"
encoded_filter = requests.utils.quote(invalid_filter)
response = requests.get(f"{self.base_url}Sag?%24filter={encoded_filter}&%24inlinecount=allpages")
self.assertEqual(response.status_code, 200)
data = response.json()
# Should return large dataset due to silent failure
self.assertGreater(int(data['odata.count']), 1000)
if __name__ == '__main__':
unittest.main()
2. Integration Testing¶
// Integration test for application compatibility
describe('API Integration Tests', () => {
test('Large dataset fetching with pagination', async () => {
const results = await fetchAllRecords('Sag', null, 100);
expect(results.length).toBeGreaterThan(100);
});
test('Complex query compatibility', async () => {
const query = "titel eq 'Forslag til folketingsbeslutning'";
const response = await robustApiCall(`/api/Sag?%24filter=${encodeURIComponent(query)}`);
expect(response.value).toBeDefined();
});
test('Error handling robustness', async () => {
// Test invalid entity
await expect(robustApiCall('/api/InvalidEntity')).rejects.toThrow();
// Test invalid expansion
await expect(robustApiCall('/api/Sag?%24expand=InvalidRelation')).rejects.toThrow();
});
});
3. Monitoring and Alerting¶
import time
import requests
import logging
class APIMonitor:
def __init__(self):
self.base_url = "https://oda.ft.dk/api/"
self.logger = logging.getLogger('api_monitor')
def check_api_health(self):
"""Monitor API health and performance"""
try:
start_time = time.time()
response = requests.get(f"{self.base_url}Sag?%24top=1", timeout=10)
response_time = (time.time() - start_time) * 1000
if response.status_code != 200:
self.logger.error(f"API health check failed: {response.status_code}")
return False
if response_time > 5000: # 5 second threshold
self.logger.warning(f"API response time high: {response_time}ms")
return True
except Exception as e:
self.logger.error(f"API health check exception: {e}")
return False
def check_pagination_limits(self):
"""Monitor for pagination limit changes"""
try:
response = requests.get(f"{self.base_url}Sag?%24top=200")
data = response.json()
actual_count = len(data['value'])
if actual_count > 100:
self.logger.warning(f"Pagination limit may have changed: got {actual_count} records")
return actual_count
except Exception as e:
self.logger.error(f"Pagination check failed: {e}")
return None
Change Notification Recommendations¶
Since the API doesn't provide native change notifications:
- Subscribe to Official Channels: Monitor ft.dk for API announcements
- Implement Health Monitoring: Regular automated API health checks
- Version Control Documentation: Track this documentation for updates
- Community Engagement: Participate in developer communities using the API
Conclusion¶
The Danish Parliamentary Open Data API has maintained remarkable stability while evolving to meet performance and reliability requirements. The most significant recent change has been the pagination limit reduction, which requires applications to implement proper pagination strategies.
Key principles for API change resilience:
- Always Use URL Encoding: Critical for OData parameter compatibility
- Implement Pagination: Never rely on large batch queries
- Validate Filters Client-Side: Compensate for silent filter failures
- Monitor API Health: Detect changes through automated testing
- Handle Errors Gracefully: Implement robust error handling patterns
By following these guidelines and staying informed about API evolution, developers can build resilient applications that adapt to changes while maintaining reliability and performance.