diff --git a/Sources/AWSLambdaEvents/APIGateway+V2.swift b/Sources/AWSLambdaEvents/APIGateway+V2.swift
index 767bd1b..42cf355 100644
--- a/Sources/AWSLambdaEvents/APIGateway+V2.swift
+++ b/Sources/AWSLambdaEvents/APIGateway+V2.swift
@@ -15,7 +15,7 @@
 import HTTPTypes
 
 /// `APIGatewayV2Request` contains data coming from the new HTTP API Gateway.
-public struct APIGatewayV2Request: Codable, Sendable {
+public struct APIGatewayV2Request: Encodable, Sendable {
     /// `Context` contains information to identify the AWS account and resources invoking the Lambda function.
     public struct Context: Codable, Sendable {
         public struct HTTP: Codable, Sendable {
@@ -96,13 +96,13 @@ public struct APIGatewayV2Request: Codable, Sendable {
     public let rawPath: String
     public let rawQueryString: String
 
-    public let cookies: [String]?
+    public let cookies: [String]
     public let headers: HTTPHeaders
-    public let queryStringParameters: [String: String]?
-    public let pathParameters: [String: String]?
+    public let queryStringParameters: [String: String]
+    public let pathParameters: [String: String]
 
     public let context: Context
-    public let stageVariables: [String: String]?
+    public let stageVariables: [String: String]
 
     public let body: String?
     public let isBase64Encoded: Bool
@@ -147,3 +147,26 @@ public struct APIGatewayV2Response: Codable, Sendable {
         self.cookies = cookies
     }
 }
+
+extension APIGatewayV2Request: Decodable {
+    public init(from decoder: Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+
+        self.version = try container.decode(String.self, forKey: .version)
+        self.routeKey = try container.decode(String.self, forKey: .routeKey)
+        self.rawPath = try container.decode(String.self, forKey: .rawPath)
+        self.rawQueryString = try container.decode(String.self, forKey: .rawQueryString)
+
+        self.cookies = try container.decodeIfPresent([String].self, forKey: .cookies) ?? []
+        self.headers = try container.decodeIfPresent(HTTPHeaders.self, forKey: .headers) ?? HTTPHeaders()
+        self.queryStringParameters =
+            try container.decodeIfPresent([String: String].self, forKey: .queryStringParameters) ?? [:]
+        self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) ?? [:]
+
+        self.context = try container.decode(Context.self, forKey: .context)
+        self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) ?? [:]
+
+        self.body = try container.decodeIfPresent(String.self, forKey: .body)
+        self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)
+    }
+}
diff --git a/Sources/AWSLambdaEvents/APIGateway.swift b/Sources/AWSLambdaEvents/APIGateway.swift
index e2a18f1..2ca51c3 100644
--- a/Sources/AWSLambdaEvents/APIGateway.swift
+++ b/Sources/AWSLambdaEvents/APIGateway.swift
@@ -24,7 +24,7 @@ import Foundation
 // https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
 
 /// `APIGatewayRequest` contains data coming from the API Gateway.
-public struct APIGatewayRequest: Codable, Sendable {
+public struct APIGatewayRequest: Encodable, Sendable {
     public struct Context: Codable, Sendable {
         public struct Identity: Codable, Sendable {
             public let cognitoIdentityPoolId: String?
@@ -64,12 +64,12 @@ public struct APIGatewayRequest: Codable, Sendable {
     public let path: String
     public let httpMethod: HTTPRequest.Method
 
-    public let queryStringParameters: [String: String]?
-    public let multiValueQueryStringParameters: [String: [String]]?
+    public let queryStringParameters: [String: String]
+    public let multiValueQueryStringParameters: [String: [String]]
     public let headers: HTTPHeaders
     public let multiValueHeaders: HTTPMultiValueHeaders
-    public let pathParameters: [String: String]?
-    public let stageVariables: [String: String]?
+    public let pathParameters: [String: String]
+    public let stageVariables: [String: String]
 
     public let requestContext: Context
     public let body: String?
@@ -99,3 +99,28 @@ public struct APIGatewayResponse: Codable, Sendable {
         self.isBase64Encoded = isBase64Encoded
     }
 }
+
+extension APIGatewayRequest: Decodable {
+    public init(from decoder: any Decoder) throws {
+        let container = try decoder.container(keyedBy: CodingKeys.self)
+
+        self.resource = try container.decode(String.self, forKey: .resource)
+        self.path = try container.decode(String.self, forKey: .path)
+        self.httpMethod = try container.decode(HTTPRequest.Method.self, forKey: .httpMethod)
+
+        self.queryStringParameters =
+            try container.decodeIfPresent([String: String].self, forKey: .queryStringParameters) ?? [:]
+        self.multiValueQueryStringParameters =
+            try container.decodeIfPresent([String: [String]].self, forKey: .multiValueQueryStringParameters) ?? [:]
+        self.headers = try container.decodeIfPresent(HTTPHeaders.self, forKey: .headers) ?? HTTPHeaders()
+        self.multiValueHeaders =
+            try container.decodeIfPresent(HTTPMultiValueHeaders.self, forKey: .multiValueHeaders)
+            ?? HTTPMultiValueHeaders()
+        self.pathParameters = try container.decodeIfPresent([String: String].self, forKey: .pathParameters) ?? [:]
+        self.stageVariables = try container.decodeIfPresent([String: String].self, forKey: .stageVariables) ?? [:]
+
+        self.requestContext = try container.decode(Context.self, forKey: .requestContext)
+        self.body = try container.decodeIfPresent(String.self, forKey: .body)
+        self.isBase64Encoded = try container.decode(Bool.self, forKey: .isBase64Encoded)
+    }
+}
diff --git a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift
index aa6d208..f3af5ab 100644
--- a/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift
+++ b/Tests/AWSLambdaEventsTests/APIGateway+V2Tests.swift
@@ -73,6 +73,46 @@ class APIGatewayV2Tests: XCTestCase {
         }
         """
 
+    static let exampleGetEventBodyNilHeaders = """
+        {
+            "routeKey":"GET /hello",
+            "version":"2.0",
+            "rawPath":"/hello",
+            "requestContext":{
+                "timeEpoch":1587750461466,
+                "domainPrefix":"hello",
+                "authorizer":{
+                    "jwt":{
+                        "scopes":[
+                            "hello"
+                        ],
+                        "claims":{
+                            "aud":"customers",
+                            "iss":"https://hello.test.com/",
+                            "iat":"1587749276",
+                            "exp":"1587756476"
+                        }
+                    }
+                },
+                "accountId":"0123456789",
+                "stage":"$default",
+                "domainName":"hello.test.com",
+                "apiId":"pb5dg6g3rg",
+                "requestId":"LgLpnibOFiAEPCA=",
+                "http":{
+                    "path":"/hello",
+                    "userAgent":"Paw/3.1.10 (Macintosh; OS X/10.15.4) GCDHTTPRequest",
+                    "method":"GET",
+                    "protocol":"HTTP/1.1",
+                    "sourceIp":"91.64.117.86"
+                },
+                "time":"24/Apr/2020:17:47:41 +0000"
+            },
+            "isBase64Encoded":false,
+            "rawQueryString":"foo=bar"
+        }
+        """
+
     static let fullExamplePayload = """
         {
             "version": "2.0",
@@ -156,7 +196,7 @@ class APIGatewayV2Tests: XCTestCase {
 
         XCTAssertEqual(req?.rawPath, "/hello")
         XCTAssertEqual(req?.context.http.method, .get)
-        XCTAssertEqual(req?.queryStringParameters?.count, 1)
+        XCTAssertEqual(req?.queryStringParameters.count, 1)
         XCTAssertEqual(req?.rawQueryString, "foo=bar")
         XCTAssertEqual(req?.headers.count, 8)
         XCTAssertEqual(req?.context.authorizer?.jwt?.claims?["aud"], "customers")
@@ -176,4 +216,9 @@ class APIGatewayV2Tests: XCTestCase {
         XCTAssertEqual(clientCert?.validity.notBefore, "May 28 12:30:02 2019 GMT")
         XCTAssertEqual(clientCert?.validity.notAfter, "Aug  5 09:36:04 2021 GMT")
     }
+
+    func testDecodingNilCollections() {
+        let data = APIGatewayV2Tests.exampleGetEventBodyNilHeaders.data(using: .utf8)!
+        XCTAssertNoThrow(_ = try JSONDecoder().decode(APIGatewayV2Request.self, from: data))
+    }
 }
diff --git a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift
index 517e4b1..2f55dfe 100644
--- a/Tests/AWSLambdaEventsTests/APIGatewayTests.swift
+++ b/Tests/AWSLambdaEventsTests/APIGatewayTests.swift
@@ -34,6 +34,10 @@ class APIGatewayTests: XCTestCase {
           {"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource": "/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "domainName": "1234567890.execute-api.us-east-1.amazonaws.com", "resourcePath": "/todos", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "test", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/todos"}, "queryStringParameters": null, "multiValueQueryStringParameters": null, "headers": {"Host": "127.0.0.1:3000", "Connection": "keep-alive", "Content-Length": "18", "Pragma": "no-cache", "Cache-Control": "no-cache", "Accept": "text/plain, */*; q=0.01", "Origin": "http://todobackend.com", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25", "Dnt": "1", "Content-Type": "application/json", "Sec-Fetch-Site": "cross-site", "Sec-Fetch-Mode": "cors", "Referer": "http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "X-Forwarded-Proto": "http", "X-Forwarded-Port": "3000"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Content-Length": ["18"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"], "Accept": ["text/plain, */*; q=0.01"], "Origin": ["http://todobackend.com"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25"], "Dnt": ["1"], "Content-Type": ["application/json"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-Mode": ["cors"], "Referer": ["http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "pathParameters": null, "stageVariables": null, "path": "/todos", "isBase64Encoded": false}
         """
 
+    static let postEventBodyNilHeaders = """
+          {"httpMethod": "POST", "body": "{\\"title\\":\\"a todo\\"}", "resource":"/todos", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "domainName": "1234567890.execute-api.us-east-1.amazonaws.com", "resourcePath": "/todos", "httpMethod": "POST", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "test", "identity": {"apiKey": null, "userArn": null,"cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/todos"}, "multiValueHeaders": {"Host": ["127.0.0.1:3000"], "Connection": ["keep-alive"], "Content-Length": ["18"], "Pragma": ["no-cache"], "Cache-Control": ["no-cache"], "Accept": ["text/plain, */*; q=0.01"], "Origin": ["http://todobackend.com"], "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.36 Safari/537.36 Edg/79.0.309.25"], "Dnt": ["1"], "Content-Type": ["application/json"], "Sec-Fetch-Site": ["cross-site"], "Sec-Fetch-Mode": ["cors"], "Referer": ["http://todobackend.com/specs/index.html?http://127.0.0.1:3000/todos"], "Accept-Encoding": ["gzip, deflate, br"], "Accept-Language": ["en-US,en;q=0.9"], "X-Forwarded-Proto": ["http"], "X-Forwarded-Port": ["3000"]}, "path": "/todos", "isBase64Encoded": false}
+        """
+
     // MARK: - Request -
 
     // MARK: Decoding
@@ -108,4 +112,9 @@ class APIGatewayTests: XCTestCase {
         XCTAssertEqual(json?.isBase64Encoded, resp.isBase64Encoded)
         XCTAssertEqual(json?.headers?["Server"], "Test")
     }
+
+    func testDecodingNilCollections() {
+        let data = APIGatewayTests.postEventBodyNilHeaders.data(using: .utf8)!
+        XCTAssertNoThrow(_ = try JSONDecoder().decode(APIGatewayRequest.self, from: data))
+    }
 }