diff --git a/Networking/Networking.xcodeproj/project.pbxproj b/Networking/Networking.xcodeproj/project.pbxproj
index bcbd1c6afb2..e9f338ddaf6 100644
--- a/Networking/Networking.xcodeproj/project.pbxproj
+++ b/Networking/Networking.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 53;
+	objectVersion = 54;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -2887,113 +2887,113 @@
 		B567AF2720A0FA0A00AB6C62 /* Mapper */ = {
 			isa = PBXGroup;
 			children = (
-				EE57C10F297927A000BC31E7 /* ApplicationPassword */,
-				B567AF2820A0FA1E00AB6C62 /* Mapper.swift */,
 				B505F6CC20BEE37E00BB1B69 /* AccountMapper.swift */,
 				93D8BBFC226BBEE800AD2EB3 /* AccountSettingsMapper.swift */,
 				2685C0F9263B5D5300D9EE97 /* AddOnGroupMapper.swift */,
+				EE839C252ABEF9C900049545 /* AIProductMapper.swift */,
 				DE4D23BB29B5FC0D003A4B5D /* AnnouncementListMapper.swift */,
+				EE57C10F297927A000BC31E7 /* ApplicationPassword */,
 				740211E221939C83002248DA /* CommentResultMapper.swift */,
+				45150A9D26836A57006922EA /* CountryListMapper.swift */,
 				03DCB73F2624AD7D00C8953D /* CouponListMapper.swift */,
 				03DCB785262739D200C8953D /* CouponMapper.swift */,
 				DE2095BE279583A100171F1C /* CouponReportListMapper.swift */,
-				45150A9D26836A57006922EA /* CountryListMapper.swift */,
+				68CB800D28D8901B00E169F8 /* CustomerMapper.swift */,
 				035BA3A929113CBD0056F0AD /* DataBoolMapper.swift */,
 				B524193E21AC5FE400D6FC0A /* DotcomDeviceMapper.swift */,
 				CE865A9C2A41E1480049B03C /* EntityDateModifiedMapper.swift */,
 				2676F4CD290AE6BB00C7A15B /* EntityIDMapper.swift */,
-				DE50295C28C6068B00551736 /* JetpackUserMapper.swift */,
 				AEF9458A27297FF6001DCCFB /* IgnoringResponseMapper.swift */,
-				E18152BF28F85D4A0011A0EC /* InAppPurchasesProductMapper.swift */,
 				E16C59B428F8640B007D55BB /* InAppPurchaseOrderResultMapper.swift */,
+				E18152BF28F85D4A0011A0EC /* InAppPurchasesProductMapper.swift */,
 				6846B0142A619A5C008EB143 /* InAppPurchasesTransactionMapper.swift */,
-				4513382327A951B300AE5E78 /* InboxNoteMapper.swift */,
 				45CCFCE527A2E3710012E8CB /* InboxNoteListMapper.swift */,
+				4513382327A951B300AE5E78 /* InboxNoteMapper.swift */,
+				DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */,
+				DE50295C28C6068B00551736 /* JetpackUserMapper.swift */,
 				036563DC29069BE400D84BFD /* JustInTimeMessageListMapper.swift */,
+				B567AF2820A0FA1E00AB6C62 /* Mapper.swift */,
 				020D07BD23D8570800FD9580 /* MediaListMapper.swift */,
 				D823D904223746CE00C90817 /* NewShipmentTrackingMapper.swift */,
 				B554FA8E2180BC7000C54DFF /* NoteHashListMapper.swift */,
 				B59325CB217E2B4C000B0E8E /* NoteListMapper.swift */,
-				B5C6FCD320A373BA00A4F8E4 /* OrderMapper.swift */,
 				B567AF2A20A0FA4200AB6C62 /* OrderListMapper.swift */,
-				74C8F06720EEB7BC00B6EDC9 /* OrderNotesMapper.swift */,
+				B5C6FCD320A373BA00A4F8E4 /* OrderMapper.swift */,
 				CE583A0D2109154500D73C1C /* OrderNoteMapper.swift */,
+				74C8F06720EEB7BC00B6EDC9 /* OrderNotesMapper.swift */,
 				02C254B825637BA000A04423 /* OrderShippingLabelListMapper.swift */,
 				D8FBFF1022D3B3FC006E3336 /* OrderStatsV4Mapper.swift */,
 				26731336255ACA850026F7EF /* PaymentGatewayListMapper.swift */,
 				0313651828AE559D00EEE571 /* PaymentGatewayMapper.swift */,
-				74749B96224134FF005C4CF2 /* ProductMapper.swift */,
-				AE1950F2296DB2C2004D37D2 /* ProductsBulkUpdateMapper.swift */,
-				45152810257A81730076B03C /* ProductAttributeMapper.swift */,
+				453305E82459DF2100264E50 /* PostMapper.swift */,
 				4515280C257A7EEC0076B03C /* ProductAttributeListMapper.swift */,
-				26B6453F259BCDFE00EF3FB3 /* ProductAttributeTermMapper.swift */,
+				45152810257A81730076B03C /* ProductAttributeMapper.swift */,
 				26B64543259BCE0F00EF3FB3 /* ProductAttributeTermListMapper.swift */,
+				26B6453F259BCDFE00EF3FB3 /* ProductAttributeTermMapper.swift */,
+				26615474242D7C9500A31661 /* ProductCategoryListMapper.swift */,
+				45B204B72489095100FE6526 /* ProductCategoryMapper.swift */,
 				CCF434632906BD7200B4475A /* ProductIDMapper.swift */,
 				CE0A0F18223987DF0075ED8D /* ProductListMapper.swift */,
-				45B204B72489095100FE6526 /* ProductCategoryMapper.swift */,
-				26615474242D7C9500A31661 /* ProductCategoryListMapper.swift */,
+				74749B96224134FF005C4CF2 /* ProductMapper.swift */,
 				D88D5A4A230BCF0A007B6E01 /* ProductReviewListMapper.swift */,
 				D8C251CF230BD72700F49782 /* ProductReviewMapper.swift */,
-				4599FC5724A624BD0056157A /* ProductTagListMapper.swift */,
-				0219B03823964BB3007DCD5E /* ProductShippingClassMapper.swift */,
+				AE1950F2296DB2C2004D37D2 /* ProductsBulkUpdateMapper.swift */,
 				025CA2C1238EBBAA00B05C81 /* ProductShippingClassListMapper.swift */,
+				0219B03823964BB3007DCD5E /* ProductShippingClassMapper.swift */,
 				45D685F723D0BC78005F87D0 /* ProductSkuMapper.swift */,
 				CE71E2302A4C3DDA00DB5376 /* ProductsReportMapper.swift */,
-				CE430673234BA6AD0073CBFF /* RefundMapper.swift */,
+				020EF5E82A8BC957009D2169 /* ProductsTotalMapper.swift */,
+				4599FC5724A624BD0056157A /* ProductTagListMapper.swift */,
+				026CF61D237D6985009563D4 /* ProductVariationListMapper.swift */,
+				02C1CEF324C6A02B00703EBA /* ProductVariationMapper.swift */,
+				26BD9FCE2965EE71004E0D15 /* ProductVariationsBulkCreateMapper.swift */,
+				09EA564A27C75FCE00407D40 /* ProductVariationsBulkUpdateMapper.swift */,
+				D8EDFE2525EE8A60003D2213 /* ReaderConnectionTokenMapper.swift */,
 				CE430675234BA7920073CBFF /* RefundListMapper.swift */,
+				CE430673234BA6AD0073CBFF /* RefundMapper.swift */,
+				31054701262E04F700C5C02B /* RemotePaymentIntentMapper.swift */,
+				3178A49E2703E5CF00A8B4CA /* RemoteReaderLocationMapper.swift */,
 				7412A8EB21B6E286005D182A /* ReportOrderTotalsMapper.swift */,
 				743E84ED2217244C00FAC9D7 /* ShipmentTrackingListMapper.swift */,
 				D823D91122377DF200C90817 /* ShipmentTrackingProviderListMapper.swift */,
+				CCF48B272628A4EB0034EA83 /* ShippingLabelAccountSettingsMapper.swift */,
+				45A4B84D25D2E11300776FB4 /* ShippingLabelAddressValidationSuccessMapper.swift */,
+				456930AA264EB85A009ED69D /* ShippingLabelCarriersAndRatesMapper.swift */,
+				CC9A24F32642CF37005DE56E /* ShippingLabelCreationEligibilityMapper.swift */,
+				451A9831260B9D2D0059D135 /* ShippingLabelPackagesMapper.swift */,
+				029BA53A255DFABD006171FD /* ShippingLabelPrintDataMapper.swift */,
+				CC0786602677B2DA00BA9AC1 /* ShippingLabelPurchaseMapper.swift */,
+				021A84D9257DF92800BC71D1 /* ShippingLabelRefundMapper.swift */,
+				CC0786C4267BAF0F00BA9AC1 /* ShippingLabelStatusMapper.swift */,
 				7426CA1021AF30BD004E9FFC /* SiteAPIMapper.swift */,
 				B56C1EB520EA757B00D749F9 /* SiteListMapper.swift */,
 				CE50345D21B571A7007573C6 /* SitePlanMapper.swift */,
-				453305E82459DF2100264E50 /* PostMapper.swift */,
-				31D27C8626028CE9002EDB1D /* SitePluginsMapper.swift */,
 				DEC51A94274CDA52009F3DF4 /* SitePluginMapper.swift */,
-				74046E1E217A6B70007DD7BF /* SiteSettingsMapper.swift */,
+				31D27C8626028CE9002EDB1D /* SitePluginsMapper.swift */,
 				DE74F29927E08F5A0002FE59 /* SiteSettingMapper.swift */,
-				CE12AE9629F2AB3F0056DD17 /* SubscriptionMapper.swift */,
-				CCE5F38C29EFFBC400087332 /* SubscriptionListMapper.swift */,
-				B53EF53B21814900003E146F /* SuccessResultMapper.swift */,
-				74A1D26C21189DFE00931DFA /* SiteVisitStatsMapper.swift */,
+				74046E1E217A6B70007DD7BF /* SiteSettingsMapper.swift */,
 				CCA1D60529437D8F00B40560 /* SiteSummaryStatsMapper.swift */,
+				74A1D26C21189DFE00931DFA /* SiteVisitStatsMapper.swift */,
+				EE2C09C129AF26B2009396F9 /* StoreOnboardingTaskListMapper.swift */,
+				311D412D2783C07D00052F64 /* StripeAccountMapper.swift */,
+				CCE5F38C29EFFBC400087332 /* SubscriptionListMapper.swift */,
+				CE12AE9629F2AB3F0056DD17 /* SubscriptionMapper.swift */,
 				CCB2CA9D262091CB00285CA0 /* SuccessDataResultMapper.swift */,
+				B53EF53B21814900003E146F /* SuccessResultMapper.swift */,
+				DEC51AE827687AAF009F3DF4 /* SystemPluginMapper.swift */,
+				077F39D326A58DE700ABEADC /* SystemStatusMapper.swift */,
 				450106902399B2C800E24722 /* TaxClassListMapper.swift */,
+				B9DFE4BD2AA2057B00174004 /* TaxRateListMapper.swift */,
+				B9CFF6512AB2118900C2F616 /* TaxRateMapper.swift */,
 				74ABA1D2213F25AE00FFAD30 /* TopEarnerStatsMapper.swift */,
-				026CF61D237D6985009563D4 /* ProductVariationListMapper.swift */,
-				02C1CEF324C6A02B00703EBA /* ProductVariationMapper.swift */,
-				09EA564A27C75FCE00407D40 /* ProductVariationsBulkUpdateMapper.swift */,
-				26BD9FCE2965EE71004E0D15 /* ProductVariationsBulkCreateMapper.swift */,
-				451A9831260B9D2D0059D135 /* ShippingLabelPackagesMapper.swift */,
-				029BA53A255DFABD006171FD /* ShippingLabelPrintDataMapper.swift */,
-				021A84D9257DF92800BC71D1 /* ShippingLabelRefundMapper.swift */,
-				311D412D2783C07D00052F64 /* StripeAccountMapper.swift */,
-				3192F21B260D32550067FEF9 /* WCPayAccountMapper.swift */,
-				D8EDFE2525EE8A60003D2213 /* ReaderConnectionTokenMapper.swift */,
-				3178A49E2703E5CF00A8B4CA /* RemoteReaderLocationMapper.swift */,
-				31054701262E04F700C5C02B /* RemotePaymentIntentMapper.swift */,
-				45A4B84D25D2E11300776FB4 /* ShippingLabelAddressValidationSuccessMapper.swift */,
-				CCF48B272628A4EB0034EA83 /* ShippingLabelAccountSettingsMapper.swift */,
-				456930AA264EB85A009ED69D /* ShippingLabelCarriersAndRatesMapper.swift */,
-				CC9A24F32642CF37005DE56E /* ShippingLabelCreationEligibilityMapper.swift */,
-				CC0786602677B2DA00BA9AC1 /* ShippingLabelPurchaseMapper.swift */,
-				CC0786C4267BAF0F00BA9AC1 /* ShippingLabelStatusMapper.swift */,
 				FE28F6E326842848004465C7 /* UserMapper.swift */,
-				077F39D326A58DE700ABEADC /* SystemStatusMapper.swift */,
-				DEC51AE827687AAF009F3DF4 /* SystemPluginMapper.swift */,
+				68F48B0C28E3B2E80045C15B /* WCAnalyticsCustomerMapper.swift */,
+				3192F21B260D32550067FEF9 /* WCPayAccountMapper.swift */,
+				0359EA1C27AADE000048DE2D /* WCPayChargeMapper.swift */,
 				02C11275274285FF00F4F0B4 /* WooCommerceAvailabilityMapper.swift */,
 				02BE0A7A274B695F001176D2 /* WordPressMediaMapper.swift */,
-				02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */,
-				0359EA1C27AADE000048DE2D /* WCPayChargeMapper.swift */,
-				DE34051828BDEE6A00CF0D97 /* JetpackConnectionURLMapper.swift */,
-				68CB800D28D8901B00E169F8 /* CustomerMapper.swift */,
-				68F48B0C28E3B2E80045C15B /* WCAnalyticsCustomerMapper.swift */,
 				DE2E8E9E295310C5002E4B14 /* WordPressSiteMapper.swift */,
-				EE2C09C129AF26B2009396F9 /* StoreOnboardingTaskListMapper.swift */,
-				020EF5E82A8BC957009D2169 /* ProductsTotalMapper.swift */,
-				B9DFE4BD2AA2057B00174004 /* TaxRateListMapper.swift */,
-				B9CFF6512AB2118900C2F616 /* TaxRateMapper.swift */,
-				EE839C252ABEF9C900049545 /* AIProductMapper.swift */,
+				02C112772742862600F4F0B4 /* WordPressSiteSettingsMapper.swift */,
 			);
 			path = Mapper;
 			sourceTree = "<group>";
diff --git a/Networking/Networking/Extensions/Mapper+DataEnvelope.swift b/Networking/Networking/Extensions/Mapper+DataEnvelope.swift
index 8f94ef013eb..4a02f723208 100644
--- a/Networking/Networking/Extensions/Mapper+DataEnvelope.swift
+++ b/Networking/Networking/Extensions/Mapper+DataEnvelope.swift
@@ -1,25 +1,12 @@
-import Foundation
-
 extension Mapper {
+
     /// Checks whether the JSON data has a `data` key at the root.
-    ///
     func hasDataEnvelope(in response: Data) -> Bool {
-        let decoder = JSONDecoder()
         do {
-            _ = try decoder.decode(ContentEnvelope.self, from: response)
+            _ = try JSONDecoder().decode(Envelope<AnyDecodable>.self, from: response)
             return true
         } catch {
             return false
         }
     }
 }
-
-/// Helper struct to attempt parsing some JSON data with a `data` key at the root.
-///
-private struct ContentEnvelope: Decodable {
-    let content: AnyDecodable
-
-    private enum CodingKeys: String, CodingKey {
-        case content = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/AccountMapper.swift b/Networking/Networking/Mapper/AccountMapper.swift
index 20411c9b503..b7b886cdec9 100644
--- a/Networking/Networking/Mapper/AccountMapper.swift
+++ b/Networking/Networking/Mapper/AccountMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Account
 ///
 class AccountMapper: Mapper {
diff --git a/Networking/Networking/Mapper/AccountSettingsMapper.swift b/Networking/Networking/Mapper/AccountSettingsMapper.swift
index f98a369bb60..703f41f99d5 100644
--- a/Networking/Networking/Mapper/AccountSettingsMapper.swift
+++ b/Networking/Networking/Mapper/AccountSettingsMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: AccountSettings
 ///
 struct AccountSettingsMapper: Mapper {
diff --git a/Networking/Networking/Mapper/AddOnGroupMapper.swift b/Networking/Networking/Mapper/AddOnGroupMapper.swift
index faaa7f4fe70..464d23d3979 100644
--- a/Networking/Networking/Mapper/AddOnGroupMapper.swift
+++ b/Networking/Networking/Mapper/AddOnGroupMapper.swift
@@ -1,27 +1 @@
-import Foundation
-
-/// Maps between a raw json response to an array of `AddOnGroups`
-///
-struct AddOnGroupMapper: Mapper {
-    /// Site Identifier associated to the `AddOnGroup` that will be parsed.
-    /// We're injecting this field via `JSONDecoder.userInfo` because `SiteID` is not returned in any of the `AddOnGroup` endpoints.
-    ///
-    let siteID: Int64
-
-    func map(response: Data) throws -> [AddOnGroup] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [.siteID: siteID]
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(AddOnGroupEnvelope.self, from: response).data
-        } else {
-            return try decoder.decode([AddOnGroup].self, from: response)
-        }
-    }
-}
-
-/// `AddOnGroupEnvelope` Disposable Entity:
-/// `AddOnGroup` endpoints returns it's add-on-groups json in the `data` key.
-///
-private struct AddOnGroupEnvelope: Decodable {
-    let data: [AddOnGroup]
-}
+typealias AddOnGroupMapper = SiteIDMapper<[AddOnGroup]>
diff --git a/Networking/Networking/Mapper/AnnouncementListMapper.swift b/Networking/Networking/Mapper/AnnouncementListMapper.swift
index e1ded913642..44442becaec 100644
--- a/Networking/Networking/Mapper/AnnouncementListMapper.swift
+++ b/Networking/Networking/Mapper/AnnouncementListMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper for `[Announcement]`
 struct AnnouncementListMapper: Mapper {
 
diff --git a/Networking/Networking/Mapper/CommentResultMapper.swift b/Networking/Networking/Mapper/CommentResultMapper.swift
index 11e03ed320e..7c48e8aaa6f 100644
--- a/Networking/Networking/Mapper/CommentResultMapper.swift
+++ b/Networking/Networking/Mapper/CommentResultMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Comment Moderation Result
 ///
 struct CommentResultMapper: Mapper {
@@ -8,7 +5,6 @@ struct CommentResultMapper: Mapper {
     /// (Attempts) to extract the updated `status` field from a given JSON Encoded response.
     ///
     func map(response: Data) throws -> CommentStatus {
-
         let dictionary = try JSONDecoder().decode([String: AnyDecodable].self, from: response)
         let status = (dictionary[Constants.statusKey]?.value as? String) ?? ""
         return CommentStatus(rawValue: status) ?? .unknown
diff --git a/Networking/Networking/Mapper/CountryListMapper.swift b/Networking/Networking/Mapper/CountryListMapper.swift
index dc7e7f00934..92916f96d49 100644
--- a/Networking/Networking/Mapper/CountryListMapper.swift
+++ b/Networking/Networking/Mapper/CountryListMapper.swift
@@ -1,29 +1,8 @@
-import Foundation
-
-
-/// Mapper: Country Collection
-///
 struct CountryListMapper: Mapper {
 
     /// (Attempts) to convert an instance of Data into an array of Country Entities.
     ///
     func map(response: Data) throws -> [Country] {
-        if hasDataEnvelope(in: response) {
-            return try JSONDecoder().decode(CountryListEnvelope.self, from: response).data
-        } else {
-            return try JSONDecoder().decode([Country].self, from: response)
-        }
-    }
-}
-
-
-/// CountryListEnvelope Disposable Entity:
-/// This entity allows us to parse [Country] with JSONDecoder.
-///
-private struct CountryListEnvelope: Decodable {
-    let data: [Country]
-
-    private enum CodingKeys: String, CodingKey {
-        case data
+        try extract(from: response)
     }
 }
diff --git a/Networking/Networking/Mapper/CouponListMapper.swift b/Networking/Networking/Mapper/CouponListMapper.swift
index 757ccb3ab23..51c94e3a2e6 100644
--- a/Networking/Networking/Mapper/CouponListMapper.swift
+++ b/Networking/Networking/Mapper/CouponListMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: `Coupon` List
 ///
 struct CouponListMapper: Mapper {
@@ -13,7 +11,7 @@ struct CouponListMapper: Mapper {
     func map(response: Data) throws -> [Coupon] {
         let decoder = Coupon.decoder
         if hasDataEnvelope(in: response) {
-            let coupons = try decoder.decode(CouponListEnvelope.self, from: response).coupons
+            let coupons = try decoder.decode(Envelope<[Coupon]>.self, from: response).data
             return coupons.map { $0.copy(siteID: siteID) }
         } else {
             return try decoder.decode([Coupon].self, from: response)
@@ -21,16 +19,3 @@ struct CouponListMapper: Mapper {
         }
     }
 }
-
-
-/// CouponListEnvelope Disposable Entity:
-/// Load All Coupons endpoint returns the coupons in the `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct CouponListEnvelope: Decodable {
-    let coupons: [Coupon]
-
-    private enum CodingKeys: String, CodingKey {
-        case coupons = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/CouponMapper.swift b/Networking/Networking/Mapper/CouponMapper.swift
index adb3927fc33..8b0dd0d531b 100644
--- a/Networking/Networking/Mapper/CouponMapper.swift
+++ b/Networking/Networking/Mapper/CouponMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: `Coupon`
 ///
 struct CouponMapper: Mapper {
@@ -13,23 +11,10 @@ struct CouponMapper: Mapper {
     func map(response: Data) throws -> Coupon {
         let decoder = Coupon.decoder
         if hasDataEnvelope(in: response) {
-            let coupon = try decoder.decode(CouponEnvelope.self, from: response).coupon
+            let coupon = try decoder.decode(Envelope<Coupon>.self, from: response).data
             return coupon.copy(siteID: siteID)
         } else {
             return try decoder.decode(Coupon.self, from: response).copy(siteID: siteID)
         }
     }
 }
-
-
-/// CouponEnvelope Disposable Entity:
-/// Load Coupon endpoint returns the coupon in the `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct CouponEnvelope: Decodable {
-    let coupon: Coupon
-
-    private enum CodingKeys: String, CodingKey {
-        case coupon = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/CouponReportListMapper.swift b/Networking/Networking/Mapper/CouponReportListMapper.swift
index 0dbdec1a45d..d73ff139dde 100644
--- a/Networking/Networking/Mapper/CouponReportListMapper.swift
+++ b/Networking/Networking/Mapper/CouponReportListMapper.swift
@@ -1,30 +1,8 @@
-import Foundation
-
-/// Mapper: `CouponReport`
-///
 struct CouponReportListMapper: Mapper {
 
     /// (Attempts) to convert a dictionary into `[CouponReport]`.
     ///
     func map(response: Data) throws -> [CouponReport] {
-        let decoder = JSONDecoder()
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(CouponReportsEnvelope.self, from: response).reports
-        } else {
-            return try decoder.decode([CouponReport].self, from: response)
-        }
-    }
-}
-
-
-/// CouponReportsEnvelope Disposable Entity:
-/// Load Coupon endpoint returns the coupon in the `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct CouponReportsEnvelope: Decodable {
-    let reports: [CouponReport]
-
-    private enum CodingKeys: String, CodingKey {
-        case reports = "data"
+        try extract(from: response)
     }
 }
diff --git a/Networking/Networking/Mapper/CustomerMapper.swift b/Networking/Networking/Mapper/CustomerMapper.swift
index cef9b807b5d..84044c1c5bb 100644
--- a/Networking/Networking/Mapper/CustomerMapper.swift
+++ b/Networking/Networking/Mapper/CustomerMapper.swift
@@ -1,29 +1 @@
-import Foundation
-
-/// Mapper: Customer
-///
-struct CustomerMapper: Mapper {
-    /// We're injecting this field by copying it in after parsing responses, because `siteID` is not returned in any of the Customer endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into a `Customer` entity
-    ///
-    func map(response: Data) throws -> Customer {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [.siteID: siteID]
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(CustomerEnvelope.self, from: response).customer
-        } else {
-            return try decoder.decode(Customer.self, from: response)
-        }
-    }
-}
-
-private struct CustomerEnvelope: Decodable {
-    let customer: Customer
-
-    private enum CodingKeys: String, CodingKey {
-        case customer = "data"
-    }
-}
+typealias CustomerMapper = SiteIDMapper<Customer>
diff --git a/Networking/Networking/Mapper/DataBoolMapper.swift b/Networking/Networking/Mapper/DataBoolMapper.swift
index d731855ab77..f5ee46fcfd4 100644
--- a/Networking/Networking/Mapper/DataBoolMapper.swift
+++ b/Networking/Networking/Mapper/DataBoolMapper.swift
@@ -1,29 +1,8 @@
-import Foundation
-
-/// Mapper: Bool Result, Wrapped in `data` Key or not
-///
 struct DataBoolMapper: Mapper {
 
     /// (Attempts) to extract the boolean flag from a given JSON Encoded response.
     ///
     func map(response: Data) throws -> Bool {
-        if hasDataEnvelope(in: response) {
-            return try JSONDecoder().decode(DataBool.self, from: response).data
-        } else {
-            return try JSONDecoder().decode(Bool.self, from: response)
-        }
-    }
-}
-
-/// DataBoolResultEnvelope Disposable Entity
-///
-/// Some endpoints return a Bool response in the `data` key. This entity
-/// allows us to parse that response with JSONDecoder.
-///
-private struct DataBool: Decodable {
-    let data: Bool
-
-    private enum CodingKeys: String, CodingKey {
-        case data
+        try extract(from: response)
     }
 }
diff --git a/Networking/Networking/Mapper/DotcomDeviceMapper.swift b/Networking/Networking/Mapper/DotcomDeviceMapper.swift
index 21d7779af1a..67be99d702d 100644
--- a/Networking/Networking/Mapper/DotcomDeviceMapper.swift
+++ b/Networking/Networking/Mapper/DotcomDeviceMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Dotcom Device
 ///
 struct DotcomDeviceMapper: Mapper {
diff --git a/Networking/Networking/Mapper/EntityDateModifiedMapper.swift b/Networking/Networking/Mapper/EntityDateModifiedMapper.swift
index 5ed33ce670b..be3e13038b1 100644
--- a/Networking/Networking/Mapper/EntityDateModifiedMapper.swift
+++ b/Networking/Networking/Mapper/EntityDateModifiedMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: Date Modified for an entity, Wrapped in `data` Key or not
 ///
 struct EntityDateModifiedMapper: Mapper {
@@ -10,22 +8,14 @@ struct EntityDateModifiedMapper: Mapper {
         let decoder = JSONDecoder()
         decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
 
+        let entity: ModifiedEntity
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ModifiedEntityEnvelope.self, from: response).modifiedEntity.dateModified
+            entity = try decoder.decode(Envelope<ModifiedEntity>.self, from: response).data
         } else {
-            return try decoder.decode(ModifiedEntity.self, from: response).dateModified
+            entity = try decoder.decode(ModifiedEntity.self, from: response)
         }
-    }
-}
-
-/// Disposable Entity:
-/// Allows us to parse the date modified with JSONDecoder.
-///
-private struct ModifiedEntityEnvelope: Decodable {
-    let modifiedEntity: ModifiedEntity
 
-    private enum CodingKeys: String, CodingKey {
-        case modifiedEntity = "data"
+        return entity.dateModified
     }
 }
 
diff --git a/Networking/Networking/Mapper/EntityIDMapper.swift b/Networking/Networking/Mapper/EntityIDMapper.swift
index a061df86aed..a6a902ea296 100644
--- a/Networking/Networking/Mapper/EntityIDMapper.swift
+++ b/Networking/Networking/Mapper/EntityIDMapper.swift
@@ -1,4 +1,4 @@
-import Foundation
+private typealias EntityIDDictionary = [String: Int64]
 
 /// Mapper: Single Entity ID
 ///
@@ -9,37 +9,13 @@ struct EntityIDMapper: Mapper {
     func map(response: Data) throws -> Int64 {
         let decoder = JSONDecoder()
 
+        let idDictionary: EntityIDDictionary
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(EntityIDEnvelope.self, from: response).id
+            idDictionary = try decoder.decode(Envelope<EntityIDDictionary>.self, from: response).data
         } else {
-            let idDictionary = try decoder.decode(EntityIDEnvelope.EntityIDDictionaryType.self, from: response)
-            return idDictionary[Constants.idKey] ?? .zero
+            idDictionary = try decoder.decode(EntityIDDictionary.self, from: response)
         }
-    }
-}
-
-// MARK: Constants
-//
-private extension EntityIDMapper {
-    enum Constants {
-        static let idKey = "id"
-    }
-}
-
-/// Disposable Entity:
-/// Allows us to parse a product ID with JSONDecoder.
-///
-private struct EntityIDEnvelope: Decodable {
-    typealias EntityIDDictionaryType = [String: Int64]
-
-    private let data: EntityIDDictionaryType
-
-    // Extracts the entity ID from the underlying data
-    var id: Int64 {
-        data[EntityIDMapper.Constants.idKey] ?? .zero
-    }
 
-    private enum CodingKeys: String, CodingKey {
-        case data = "data"
+        return idDictionary["id"] ?? .zero
     }
 }
diff --git a/Networking/Networking/Mapper/InboxNoteListMapper.swift b/Networking/Networking/Mapper/InboxNoteListMapper.swift
index d6d027fad58..39caacbda0e 100644
--- a/Networking/Networking/Mapper/InboxNoteListMapper.swift
+++ b/Networking/Networking/Mapper/InboxNoteListMapper.swift
@@ -18,20 +18,9 @@ struct InboxNoteListMapper: Mapper {
             .siteID: siteID
         ]
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(InboxNoteListEnvelope.self, from: response).data
+            return try decoder.decode(Envelope<[InboxNote]>.self, from: response).data
         } else {
             return try decoder.decode([InboxNote].self, from: response)
         }
     }
 }
-
-/// InboxNoteListEnvelope Disposable Entity:
-/// This entity allows us to parse [InboxNote] with JSONDecoder.
-///
-private struct InboxNoteListEnvelope: Decodable {
-    let data: [InboxNote]
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
diff --git a/Networking/Networking/Mapper/InboxNoteMapper.swift b/Networking/Networking/Mapper/InboxNoteMapper.swift
index 72f14b88e37..e917fe7c094 100644
--- a/Networking/Networking/Mapper/InboxNoteMapper.swift
+++ b/Networking/Networking/Mapper/InboxNoteMapper.swift
@@ -18,20 +18,9 @@ struct InboxNoteMapper: Mapper {
             .siteID: siteID
         ]
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(InboxNoteEnvelope.self, from: response).data
+            return try decoder.decode(Envelope<InboxNote>.self, from: response).data
         } else {
             return try decoder.decode(InboxNote.self, from: response)
         }
     }
 }
-
-/// InboxNoteEnvelope Disposable Entity:
-/// This entity allows us to parse InboxNote with JSONDecoder.
-///
-private struct InboxNoteEnvelope: Decodable {
-    let data: InboxNote
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
diff --git a/Networking/Networking/Mapper/JustInTimeMessageListMapper.swift b/Networking/Networking/Mapper/JustInTimeMessageListMapper.swift
index 9e62b869161..1431d0b56f3 100644
--- a/Networking/Networking/Mapper/JustInTimeMessageListMapper.swift
+++ b/Networking/Networking/Mapper/JustInTimeMessageListMapper.swift
@@ -17,20 +17,9 @@ struct JustInTimeMessageListMapper: Mapper {
             .siteID: siteID
         ]
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(JustInTimeMessageListEnvelope.self, from: response).data
+            return try decoder.decode(Envelope<[JustInTimeMessage]>.self, from: response).data
         } else {
             return try decoder.decode([JustInTimeMessage].self, from: response)
         }
     }
 }
-
-/// JustInTimeMessageEnvelope Disposable Entity:
-/// This entity allows us to parse JustInTimeMessage with JSONDecoder.
-///
-private struct JustInTimeMessageListEnvelope: Decodable {
-    let data: [JustInTimeMessage]
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
diff --git a/Networking/Networking/Mapper/Mapper.swift b/Networking/Networking/Mapper/Mapper.swift
index 6f062d95691..259b24cd04c 100644
--- a/Networking/Networking/Mapper/Mapper.swift
+++ b/Networking/Networking/Mapper/Mapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Defines a Mapping Entity that will be used to parse a Backend Response.
 ///
 protocol Mapper {
@@ -13,3 +10,49 @@ protocol Mapper {
     ///
     func map(response: Data) throws -> Output
 }
+
+extension Mapper where Output: Decodable {
+
+    func extract(from response: Data, siteID: Int64, dateFormatter: DateFormatter? = .none) throws -> Output {
+        return try extract(
+            from: response,
+            decodingUserInfo: [.siteID: siteID],
+            dateFormatter: dateFormatter
+        )
+    }
+
+    func extract(from response: Data, decodingUserInfo: [CodingUserInfoKey: Any], dateFormatter: DateFormatter? = .none) throws -> Output {
+        let decoder = JSONDecoder()
+        decoder.userInfo = decodingUserInfo
+        if let dateFormatter {
+            decoder.dateDecodingStrategy = .formatted(dateFormatter)
+        }
+
+        return try extract(from: response, using: decoder)
+    }
+
+    func extract(from response: Data, using decoder: JSONDecoder = JSONDecoder()) throws -> Output {
+        if hasDataEnvelope(in: response) {
+            return try decoder.decode(Envelope<Output>.self, from: response).data
+        } else {
+            return try decoder.decode(Output.self, from: response)
+        }
+    }
+}
+
+/// A `Mapper` implementation for resources using a site id and default date formatter
+struct SiteIDMapper<Resource: Decodable>: Mapper {
+
+    /// Site Identifier associated to the `Resource`s that will be parsed.
+    ///
+    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the endpoints.
+    let siteID: Int64
+
+    func map(response: Data) throws -> Resource {
+        try extract(
+            from: response,
+            siteID: siteID,
+            dateFormatter: DateFormatter.Defaults.dateTimeFormatter
+        )
+    }
+}
diff --git a/Networking/Networking/Mapper/NewShipmentTrackingMapper.swift b/Networking/Networking/Mapper/NewShipmentTrackingMapper.swift
index 03565383f9c..0abbb826b90 100644
--- a/Networking/Networking/Mapper/NewShipmentTrackingMapper.swift
+++ b/Networking/Networking/Mapper/NewShipmentTrackingMapper.swift
@@ -1,5 +1,3 @@
-/// Mapper: NewShipmentTrackingMapper
-///
 struct NewShipmentTrackingMapper: Mapper {
     /// Site Identifier associated to the shipment trackings that will be parsed.
     /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't
@@ -16,25 +14,13 @@ struct NewShipmentTrackingMapper: Mapper {
     /// (Attempts) to convert a dictionary into an ShipmentTracking entity.
     ///
     func map(response: Data) throws -> ShipmentTracking {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.yearMonthDayDateFormatter)
-        decoder.userInfo = [
-            .siteID: siteID,
-            .orderID: orderID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(NewShipmentTrackingMapperEnvelope.self, from: response).shipmentTracking
-        } else {
-            return try decoder.decode(ShipmentTracking.self, from: response)
-        }
-    }
-}
-
-private struct NewShipmentTrackingMapperEnvelope: Decodable {
-    let shipmentTracking: ShipmentTracking
-
-    private enum CodingKeys: String, CodingKey {
-        case shipmentTracking = "data"
+        return try extract(
+            from: response,
+            decodingUserInfo: [
+                .siteID: siteID,
+                .orderID: orderID
+            ],
+            dateFormatter: DateFormatter.Defaults.yearMonthDayDateFormatter
+        )
     }
 }
diff --git a/Networking/Networking/Mapper/OrderListMapper.swift b/Networking/Networking/Mapper/OrderListMapper.swift
index b269d3f0f84..90c0c260e63 100644
--- a/Networking/Networking/Mapper/OrderListMapper.swift
+++ b/Networking/Networking/Mapper/OrderListMapper.swift
@@ -1,43 +1 @@
-import Foundation
-
-
-/// Mapper: OrderList
-///
-struct OrderListMapper: Mapper {
-
-    /// Site Identifier associated to the orders that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the Order Endpoints.
-    ///
-    let siteID: Int64
-
-
-    /// (Attempts) to convert a dictionary into [Order].
-    ///
-    func map(response: Data) throws -> [Order] {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(OrderListEnvelope.self, from: response).orders
-        } else {
-            return try decoder.decode([Order].self, from: response)
-        }
-    }
-}
-
-
-/// OrderList Disposable Entity:
-/// `Load All Orders` endpoint returns all of its orders within the `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct OrderListEnvelope: Decodable {
-    let orders: [Order]
-
-    private enum CodingKeys: String, CodingKey {
-        case orders = "data"
-    }
-}
+typealias OrderListMapper = SiteIDMapper<[Order]>
diff --git a/Networking/Networking/Mapper/OrderMapper.swift b/Networking/Networking/Mapper/OrderMapper.swift
index addf4440aa7..68085f64e28 100644
--- a/Networking/Networking/Mapper/OrderMapper.swift
+++ b/Networking/Networking/Mapper/OrderMapper.swift
@@ -1,43 +1 @@
-import Foundation
-
-
-/// Mapper: Order
-///
-struct OrderMapper: Mapper {
-
-    /// Site Identifier associated to the order that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the Order Endpoints.
-    ///
-    let siteID: Int64
-
-
-    /// (Attempts) to convert a dictionary into [Order].
-    ///
-    func map(response: Data) throws -> Order {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(OrderEnvelope.self, from: response).order
-        } else {
-            return try decoder.decode(Order.self, from: response)
-        }
-    }
-}
-
-
-/// OrdersEnvelope Disposable Entity
-///
-/// `Load Order` endpoint returns the requested order document in the `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct OrderEnvelope: Decodable {
-    let order: Order
-
-    private enum CodingKeys: String, CodingKey {
-        case order = "data"
-    }
-}
+typealias OrderMapper = SiteIDMapper<Order>
diff --git a/Networking/Networking/Mapper/OrderNoteMapper.swift b/Networking/Networking/Mapper/OrderNoteMapper.swift
index d93fae1b0fd..dc099bb2a45 100644
--- a/Networking/Networking/Mapper/OrderNoteMapper.swift
+++ b/Networking/Networking/Mapper/OrderNoteMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: OrderNote (Singular)
 ///
 class OrderNoteMapper: Mapper {
@@ -12,22 +9,9 @@ class OrderNoteMapper: Mapper {
         decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
 
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(OrderNoteEnvelope.self, from: response).orderNote
+            return try decoder.decode(Envelope<OrderNote>.self, from: response).data
         } else {
             return try decoder.decode(OrderNote.self, from: response)
         }
     }
 }
-
-
-/// OrderNote Disposable Entity:
-/// `Add Order Note` endpoint the single added note within the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct OrderNoteEnvelope: Decodable {
-    let orderNote: OrderNote
-
-    private enum CodingKeys: String, CodingKey {
-        case orderNote = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/OrderNotesMapper.swift b/Networking/Networking/Mapper/OrderNotesMapper.swift
index 4401ad87c3f..4f923c10ded 100644
--- a/Networking/Networking/Mapper/OrderNotesMapper.swift
+++ b/Networking/Networking/Mapper/OrderNotesMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: OrderNotes
 ///
 class OrderNotesMapper: Mapper {
@@ -12,22 +9,9 @@ class OrderNotesMapper: Mapper {
         decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
 
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(OrderNotesEnvelope.self, from: response).orderNotes
+            return try decoder.decode(Envelope<[OrderNote]>.self, from: response).data
         } else {
             return try decoder.decode([OrderNote].self, from: response)
         }
     }
 }
-
-
-/// OrderNote Disposable Entity:
-/// `Load Order Notes` endpoint returns all of its notes within the `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct OrderNotesEnvelope: Decodable {
-    let orderNotes: [OrderNote]
-
-    private enum CodingKeys: String, CodingKey {
-        case orderNotes = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/OrderShippingLabelListMapper.swift b/Networking/Networking/Mapper/OrderShippingLabelListMapper.swift
index 8cdfd1e6ca7..baededaef74 100644
--- a/Networking/Networking/Mapper/OrderShippingLabelListMapper.swift
+++ b/Networking/Networking/Mapper/OrderShippingLabelListMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// A wrapper of shipping labels and settings from `Load Shipping Labels` response.
 public struct OrderShippingLabelListResponse {
     /// A list of shipping labels.
@@ -32,25 +30,14 @@ struct OrderShippingLabelListMapper: Mapper {
             .orderID: orderID
         ]
 
-        let data: OrderShippingLabelListData = try {
-            if hasDataEnvelope(in: response) {
-                return try decoder.decode(OrderShippingLabelListEnvelope.self, from: response).data
-            } else {
-                return try decoder.decode(OrderShippingLabelListData.self, from: response)
-            }
-        }()
-        return OrderShippingLabelListResponse(shippingLabels: data.shippingLabels, settings: data.settings)
-    }
-}
-
-/// Disposable Entity:
-/// `Load Shipping Labels` endpoint returns the data in the `data` key.
-///
-private struct OrderShippingLabelListEnvelope: Decodable {
-    let data: OrderShippingLabelListData
+        let data: OrderShippingLabelListData
+        if hasDataEnvelope(in: response) {
+            data = try decoder.decode(Envelope<OrderShippingLabelListData>.self, from: response).data
+        } else {
+            data = try decoder.decode(OrderShippingLabelListData.self, from: response)
+        }
 
-    private enum CodingKeys: String, CodingKey {
-        case data = "data"
+        return OrderShippingLabelListResponse(shippingLabels: data.shippingLabels, settings: data.settings)
     }
 }
 
diff --git a/Networking/Networking/Mapper/OrderStatsV4Mapper.swift b/Networking/Networking/Mapper/OrderStatsV4Mapper.swift
index 47f6849a56b..40bc8b695ab 100644
--- a/Networking/Networking/Mapper/OrderStatsV4Mapper.swift
+++ b/Networking/Networking/Mapper/OrderStatsV4Mapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: OrderStats
 ///
 struct OrderStatsV4Mapper: Mapper {
@@ -19,29 +16,9 @@ struct OrderStatsV4Mapper: Mapper {
     /// (Attempts) to convert a dictionary into an OrderStats entity.
     ///
     func map(response: Data) throws -> OrderStatsV4 {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID,
-            .granularity: granularity
-        ]
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(OrderStatsV4Envelope.self, from: response).orderStats
-        } else {
-            return try decoder.decode(OrderStatsV4.self, from: response)
-        }
-    }
-}
-
-
-/// OrderStatsV4Envelope Disposable Entity
-///
-/// `Order Stats` endpoint returns the requested stats in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct OrderStatsV4Envelope: Decodable {
-    let orderStats: OrderStatsV4
-
-    private enum CodingKeys: String, CodingKey {
-        case orderStats = "data"
+        try extract(
+            from: response,
+            decodingUserInfo: [.siteID: siteID, .granularity: granularity]
+        )
     }
 }
diff --git a/Networking/Networking/Mapper/PaymentGatewayListMapper.swift b/Networking/Networking/Mapper/PaymentGatewayListMapper.swift
index 02d3ec9b366..064077a57cc 100644
--- a/Networking/Networking/Mapper/PaymentGatewayListMapper.swift
+++ b/Networking/Networking/Mapper/PaymentGatewayListMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper for an array of `PaymentGateway` JSON objects
 ///
 struct PaymentGatewayListMapper: Mapper {
@@ -13,26 +11,6 @@ struct PaymentGatewayListMapper: Mapper {
     /// (Attempts) to convert a dictionary into `[PaymentGateway]`
     ///
     func map(response: Data) throws -> [PaymentGateway] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID,
-        ]
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(PaymentGatewayListEnvelope.self, from: response).paymentGateways
-        } else {
-            return try decoder.decode([PaymentGateway].self, from: response)
-        }
-    }
-}
-
-/// PaymentGateway list disposable entity:
-/// `Load Payment Gateways` endpoint returns all of the gateway information within a `body` obejcts in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct PaymentGatewayListEnvelope: Decodable {
-    private enum CodingKeys: String, CodingKey {
-        case paymentGateways = "data"
+        try extract(from: response, siteID: siteID)
     }
-
-    let paymentGateways: [PaymentGateway]
 }
diff --git a/Networking/Networking/Mapper/PaymentGatewayMapper.swift b/Networking/Networking/Mapper/PaymentGatewayMapper.swift
index b5ec5d39d03..88db9c5ec2b 100644
--- a/Networking/Networking/Mapper/PaymentGatewayMapper.swift
+++ b/Networking/Networking/Mapper/PaymentGatewayMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper for a `PaymentGateway` JSON object
 ///
 struct PaymentGatewayMapper: Mapper {
@@ -18,21 +16,9 @@ struct PaymentGatewayMapper: Mapper {
             .siteID: siteID,
         ]
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(PaymentGatewayEnvelope.self, from: response).paymentGateway
+            return try decoder.decode(Envelope<PaymentGateway>.self, from: response).data
         } else {
             return try decoder.decode(PaymentGateway.self, from: response)
         }
     }
 }
-
-/// PaymentGateway list disposable entity:
-/// `Load Payment Gateway` endpoint returns all of the gateway information within a `body` obejcts in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct PaymentGatewayEnvelope: Decodable {
-    private enum CodingKeys: String, CodingKey {
-        case paymentGateway = "data"
-    }
-
-    let paymentGateway: PaymentGateway
-}
diff --git a/Networking/Networking/Mapper/ProductAttributeListMapper.swift b/Networking/Networking/Mapper/ProductAttributeListMapper.swift
index 14042c643f6..9c35e497cb6 100644
--- a/Networking/Networking/Mapper/ProductAttributeListMapper.swift
+++ b/Networking/Networking/Mapper/ProductAttributeListMapper.swift
@@ -1,39 +1 @@
-import Foundation
-
-/// Mapper: ProductAttribute List
-///
-struct ProductAttributeListMapper: Mapper {
-    /// Site Identifier associated to the `ProductAttribute`s that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the ProductAttribute Endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [ProductAttribute].
-    ///
-    func map(response: Data) throws -> [ProductAttribute] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductAttributeListEnvelope.self, from: response).productAttributes
-        } else {
-            return try decoder.decode([ProductAttribute].self, from: response)
-        }
-    }
-}
-
-
-/// ProductAttributeListEnvelope Disposable Entity:
-/// `Load All Products Attributes` endpoint returns the updated products document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductAttributeListEnvelope: Decodable {
-    let productAttributes: [ProductAttribute]
-
-    private enum CodingKeys: String, CodingKey {
-        case productAttributes = "data"
-    }
-}
+typealias ProductAttributeListMapper = SiteIDMapper<[ProductAttribute]>
diff --git a/Networking/Networking/Mapper/ProductAttributeMapper.swift b/Networking/Networking/Mapper/ProductAttributeMapper.swift
index 7d9ce2a543a..19e4398616c 100644
--- a/Networking/Networking/Mapper/ProductAttributeMapper.swift
+++ b/Networking/Networking/Mapper/ProductAttributeMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: ProductAttribute
 ///
 struct ProductAttributeMapper: Mapper {
@@ -20,23 +17,6 @@ struct ProductAttributeMapper: Mapper {
             .siteID: siteID
         ]
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductAttributeEnvelope.self, from: response).productAttribute
-        } else {
-            return try decoder.decode(ProductAttribute.self, from: response)
-        }
-    }
-}
-
-
-/// ProductAttributeEnvelope Disposable Entity:
-/// `Load Product Attribute` endpoint returns the updated products document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductAttributeEnvelope: Decodable {
-    let productAttribute: ProductAttribute
-
-    private enum CodingKeys: String, CodingKey {
-        case productAttribute = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/ProductAttributeTermListMapper.swift b/Networking/Networking/Mapper/ProductAttributeTermListMapper.swift
index ce79ed9c5b5..468df9830c9 100644
--- a/Networking/Networking/Mapper/ProductAttributeTermListMapper.swift
+++ b/Networking/Networking/Mapper/ProductAttributeTermListMapper.swift
@@ -1,38 +1 @@
-import Foundation
-
-/// Mapper: ProductAttributeTerm List
-///
-struct ProductAttributeTermListMapper: Mapper {
-    /// Site Identifier associated to the `ProductAttributeTermList`s that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the ProductAttributeTerm Endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into `[ProductAttributeTerm]`.
-    ///
-    func map(response: Data) throws -> [ProductAttributeTerm] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductAttributeTermListEnvelope.self, from: response).productAttributeTerms
-        } else {
-            return try decoder.decode([ProductAttributeTerm].self, from: response)
-        }
-    }
-}
-
-/// ProductAttributeTermListEnvelope Disposable Entity:
-/// `Load All ProductsAttributeTerm` endpoint returns the updated products document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductAttributeTermListEnvelope: Decodable {
-    let productAttributeTerms: [ProductAttributeTerm]
-
-    private enum CodingKeys: String, CodingKey {
-        case productAttributeTerms = "data"
-    }
-}
+typealias ProductAttributeTermListMapper = SiteIDMapper<[ProductAttributeTerm]>
diff --git a/Networking/Networking/Mapper/ProductAttributeTermMapper.swift b/Networking/Networking/Mapper/ProductAttributeTermMapper.swift
index fc3ae5b3708..018ecb2c425 100644
--- a/Networking/Networking/Mapper/ProductAttributeTermMapper.swift
+++ b/Networking/Networking/Mapper/ProductAttributeTermMapper.swift
@@ -1,40 +1 @@
-import Foundation
-
-/// Mapper: ProductAttributeTerm
-///
-struct ProductAttributeTermMapper: Mapper {
-
-    /// Site Identifier associated to the `ProductAttributeTerm`s that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the `ProductAttributeTerm` Endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into `ProductAttributeTerm`.
-    ///
-    func map(response: Data) throws -> ProductAttributeTerm {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductAttributeTermEnvelope.self, from: response).productAttributeTerm
-        } else {
-            return try decoder.decode(ProductAttributeTerm.self, from: response)
-        }
-    }
-}
-
-
-/// ProductAttributeTermEnvelope Disposable Entity:
-/// `Load ProductProductAttributeTerm` endpoint returns the updated products document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductAttributeTermEnvelope: Decodable {
-    let productAttributeTerm: ProductAttributeTerm
-
-    private enum CodingKeys: String, CodingKey {
-        case productAttributeTerm = "data"
-    }
-}
+typealias ProductAttributeTermMapper = SiteIDMapper<ProductAttributeTerm>
diff --git a/Networking/Networking/Mapper/ProductCategoryListMapper.swift b/Networking/Networking/Mapper/ProductCategoryListMapper.swift
index a710abab2d6..603328b50a7 100644
--- a/Networking/Networking/Mapper/ProductCategoryListMapper.swift
+++ b/Networking/Networking/Mapper/ProductCategoryListMapper.swift
@@ -1,39 +1 @@
-import Foundation
-
-/// Mapper: ProductCategory List
-///
-struct ProductCategoryListMapper: Mapper {
-    /// Site Identifier associated to the `ProductCategories`s that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the ProductCategory Endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [ProductCategory].
-    ///
-    func map(response: Data) throws -> [ProductCategory] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductCategoryListEnvelope.self, from: response).productCategories
-        } else {
-            return try decoder.decode([ProductCategory].self, from: response)
-        }
-    }
-}
-
-
-/// ProductCategoryListEnvelope Disposable Entity:
-/// `Load All Products Categories` endpoint returns the updated products document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductCategoryListEnvelope: Decodable {
-    let productCategories: [ProductCategory]
-
-    private enum CodingKeys: String, CodingKey {
-        case productCategories = "data"
-    }
-}
+typealias ProductCategoryListMapper = SiteIDMapper<[ProductCategory]>
diff --git a/Networking/Networking/Mapper/ProductCategoryMapper.swift b/Networking/Networking/Mapper/ProductCategoryMapper.swift
index b1c8b23c09b..c040e7dff76 100644
--- a/Networking/Networking/Mapper/ProductCategoryMapper.swift
+++ b/Networking/Networking/Mapper/ProductCategoryMapper.swift
@@ -1,42 +1 @@
-import Foundation
-
-
-/// Mapper: ProductCategory
-///
-struct ProductCategoryMapper: Mapper {
-
-    /// Site Identifier associated to the `ProductCategory`s that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the ProductCategory Endpoints.
-    ///
-    let siteID: Int64
-
-
-    /// (Attempts) to convert a dictionary into ProductCategory.
-    ///
-    func map(response: Data) throws -> ProductCategory {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductCategoryEnvelope.self, from: response).productCategory
-        } else {
-            return try decoder.decode(ProductCategory.self, from: response)
-        }
-    }
-}
-
-
-/// ProductCategoryEnvelope Disposable Entity:
-/// `Load Product Category` endpoint returns the updated products document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductCategoryEnvelope: Decodable {
-    let productCategory: ProductCategory
-
-    private enum CodingKeys: String, CodingKey {
-        case productCategory = "data"
-    }
-}
+typealias ProductCategoryMapper = SiteIDMapper<ProductCategory>
diff --git a/Networking/Networking/Mapper/ProductIDMapper.swift b/Networking/Networking/Mapper/ProductIDMapper.swift
index 53522b4724e..3ec710a5178 100644
--- a/Networking/Networking/Mapper/ProductIDMapper.swift
+++ b/Networking/Networking/Mapper/ProductIDMapper.swift
@@ -1,4 +1,4 @@
-import Foundation
+private typealias ProductIDs = [[String: Int64]]
 
 /// Mapper: Product IDs
 ///
@@ -9,32 +9,13 @@ struct ProductIDMapper: Mapper {
     func map(response: Data) throws -> [Int64] {
         let decoder = JSONDecoder()
 
+        let ids: ProductIDs
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductIDEnvelope.self, from: response).productIDs.compactMap { $0[Constants.idKey] }
+            ids = try decoder.decode(Envelope<ProductIDs>.self, from: response).data
         } else {
-            return try decoder.decode(ProductIDEnvelope.ProductIDs.self, from: response).compactMap { $0[Constants.idKey] }
+            ids = try decoder.decode(ProductIDs.self, from: response)
         }
-    }
-}
-
-// MARK: Constants
-//
-private extension ProductIDMapper {
-    enum Constants {
-        static let idKey = "id"
-    }
-}
-
-/// ProductIDEnvelope Disposable Entity:
-/// `Products` endpoint returns if a product exists. This entity
-/// allows us to parse the product IDs with JSONDecoder.
-///
-private struct ProductIDEnvelope: Decodable {
-    typealias ProductIDs = [[String: Int64]]
-
-    let productIDs: ProductIDs
 
-    private enum CodingKeys: String, CodingKey {
-        case productIDs = "data"
+        return ids.compactMap { $0["id"] }
     }
 }
diff --git a/Networking/Networking/Mapper/ProductListMapper.swift b/Networking/Networking/Mapper/ProductListMapper.swift
index c85fcd609e9..eb5629c2d8e 100644
--- a/Networking/Networking/Mapper/ProductListMapper.swift
+++ b/Networking/Networking/Mapper/ProductListMapper.swift
@@ -1,41 +1 @@
-import Foundation
-
-
-/// Mapper: Product List
-///
-struct ProductListMapper: Mapper {
-    /// Site Identifier associated to the products that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the Product Endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [Product].
-    ///
-    func map(response: Data) throws -> [Product] {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductListEnvelope.self, from: response).products
-        } else {
-            return try decoder.decode([Product].self, from: response)
-        }
-    }
-}
-
-
-/// ProductEnvelope Disposable Entity:
-/// `Load All Products` endpoint returns the updated products document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductListEnvelope: Decodable {
-    let products: [Product]
-
-    private enum CodingKeys: String, CodingKey {
-        case products = "data"
-    }
-}
+typealias ProductListMapper = SiteIDMapper<[Product]>
diff --git a/Networking/Networking/Mapper/ProductMapper.swift b/Networking/Networking/Mapper/ProductMapper.swift
index 933fddfdca1..525d14c9e1e 100644
--- a/Networking/Networking/Mapper/ProductMapper.swift
+++ b/Networking/Networking/Mapper/ProductMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Product
 ///
 struct ProductMapper: Mapper {
@@ -21,24 +18,6 @@ struct ProductMapper: Mapper {
             .siteID: siteID
         ]
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductEnvelope.self, from: response).product
-        } else {
-            return try decoder.decode(Product.self, from: response)
-        }
-    }
-}
-
-
-/// ProductEnvelope Disposable Entity
-///
-/// `Load Product` endpoint returns the requested product document in the `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductEnvelope: Decodable {
-    let product: Product
-
-    private enum CodingKeys: String, CodingKey {
-        case product = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/ProductReviewListMapper.swift b/Networking/Networking/Mapper/ProductReviewListMapper.swift
index f7b5729c257..5723fafac5f 100644
--- a/Networking/Networking/Mapper/ProductReviewListMapper.swift
+++ b/Networking/Networking/Mapper/ProductReviewListMapper.swift
@@ -1,40 +1 @@
-import Foundation
-
-/// Mapper: Product Reviews List
-///
-struct ProductReviewListMapper: Mapper {
-    /// Site Identifier associated to the product reviews that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the Product Endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [Product].
-    ///
-    func map(response: Data) throws -> [ProductReview] {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductReviewListEnvelope.self, from: response).productReviews
-        } else {
-            return try decoder.decode([ProductReview].self, from: response)
-        }
-    }
-}
-
-
-/// ProductReviewListEnvelope Disposable Entity:
-/// `Load All Products Reviews` endpoint returns the updated products document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductReviewListEnvelope: Decodable {
-    let productReviews: [ProductReview]
-
-    private enum CodingKeys: String, CodingKey {
-        case productReviews = "data"
-    }
-}
+typealias ProductReviewListMapper = SiteIDMapper<[ProductReview]>
diff --git a/Networking/Networking/Mapper/ProductReviewMapper.swift b/Networking/Networking/Mapper/ProductReviewMapper.swift
index 4522045435c..d91899dd0d0 100644
--- a/Networking/Networking/Mapper/ProductReviewMapper.swift
+++ b/Networking/Networking/Mapper/ProductReviewMapper.swift
@@ -1,44 +1 @@
-import Foundation
-
-
-/// Mapper: ProductReview
-///
-struct ProductReviewMapper: Mapper {
-
-    /// Site Identifier associated to the product review that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the Product Endpoints.
-    ///
-    let siteID: Int64
-
-
-    /// (Attempts) to convert a dictionary into ProductReview.
-    ///
-    func map(response: Data) throws -> ProductReview {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductReviewEnvelope.self, from: response).productReview
-        } else {
-            return try decoder.decode(ProductReview.self, from: response)
-        }
-    }
-}
-
-
-/// ProductReviewEnvelope Disposable Entity
-///
-/// `Load Product Review` endpoint returns the requested product document in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct ProductReviewEnvelope: Decodable {
-    let productReview: ProductReview
-
-    private enum CodingKeys: String, CodingKey {
-        case productReview = "data"
-    }
-}
+typealias ProductReviewMapper = SiteIDMapper<ProductReview>
diff --git a/Networking/Networking/Mapper/ProductShippingClassListMapper.swift b/Networking/Networking/Mapper/ProductShippingClassListMapper.swift
index 6cfa35415fa..d42eaf1f8bf 100644
--- a/Networking/Networking/Mapper/ProductShippingClassListMapper.swift
+++ b/Networking/Networking/Mapper/ProductShippingClassListMapper.swift
@@ -1,40 +1 @@
-import Foundation
-
-/// Mapper: ProductShippingClass List
-///
-struct ProductShippingClassListMapper: Mapper {
-    /// Site Identifier associated to the `ProductShippingClass`s that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the ProductShippingClass Endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [ProductShippingClass].
-    ///
-    func map(response: Data) throws -> [ProductShippingClass] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductShippingClassListEnvelope.self, from: response).data
-        } else {
-            return try decoder.decode([ProductShippingClass].self, from: response)
-        }
-    }
-}
-
-
-/// ProductShippingClassListEnvelope Disposable Entity
-///
-/// `Load All ProductShippingClass` endpoint returns the requested data in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct ProductShippingClassListEnvelope: Decodable {
-    let data: [ProductShippingClass]
-
-    private enum CodingKeys: String, CodingKey {
-        case data = "data"
-    }
-}
+typealias ProductShippingClassListMapper = SiteIDMapper<[ProductShippingClass]>
diff --git a/Networking/Networking/Mapper/ProductShippingClassMapper.swift b/Networking/Networking/Mapper/ProductShippingClassMapper.swift
index 24104267255..0d64b21fd87 100644
--- a/Networking/Networking/Mapper/ProductShippingClassMapper.swift
+++ b/Networking/Networking/Mapper/ProductShippingClassMapper.swift
@@ -1,40 +1,6 @@
-import Foundation
+typealias ProductShippingClassMapper = SiteIDMapper<ProductShippingClass>
 
-/// Mapper: ProductShippingClass
-///
-struct ProductShippingClassMapper: Mapper {
-    /// Site Identifier associated to the ProductShippingClass that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the ProductShippingClass Endpoints.
-    ///
-    let siteID: Int64
+struct Envelope<Resource>: Decodable where Resource: Decodable {
 
-    /// (Attempts) to convert a dictionary into ProductShippingClass.
-    ///
-    func map(response: Data) throws -> ProductShippingClass {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductShippingClassEnvelope.self, from: response).productShippingClass
-        } else {
-            return try decoder.decode(ProductShippingClass.self, from: response)
-        }
-    }
-}
-
-
-/// ProductShippingClassEnvelope Disposable Entity
-///
-/// `Load ProductShippingClass` endpoint returns the requested data in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct ProductShippingClassEnvelope: Decodable {
-    let productShippingClass: ProductShippingClass
-
-    private enum CodingKeys: String, CodingKey {
-        case productShippingClass = "data"
-    }
+    let data: Resource
 }
diff --git a/Networking/Networking/Mapper/ProductSkuMapper.swift b/Networking/Networking/Mapper/ProductSkuMapper.swift
index 922a92e1b53..86c16b68b9d 100644
--- a/Networking/Networking/Mapper/ProductSkuMapper.swift
+++ b/Networking/Networking/Mapper/ProductSkuMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Product Sku String
 ///
 struct ProductSkuMapper: Mapper {
@@ -10,32 +7,15 @@ struct ProductSkuMapper: Mapper {
     func map(response: Data) throws -> String {
         let decoder = JSONDecoder()
 
+        let skus: ProductsSKUs
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductSkuEnvelope.self, from: response).productsSkus.first?[Constants.skuKey] ?? ""
+            skus = try decoder.decode(Envelope<ProductsSKUs>.self, from: response).data
         } else {
-            return try decoder.decode(ProductSkuEnvelope.ProductsSkus.self, from: response).first?[Constants.skuKey] ?? ""
+            skus = try decoder.decode(ProductsSKUs.self, from: response)
         }
-    }
-}
 
-// MARK: Constants
-//
-private extension ProductSkuMapper {
-    enum Constants {
-        static let skuKey = "sku"
+        return skus.first?["sku"] ?? ""
     }
 }
 
-/// ProductSkuEnvelope Disposable Entity:
-/// `Products` endpoint returns if a sku exists. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductSkuEnvelope: Decodable {
-    typealias ProductsSkus = [[String: String]]
-
-    let productsSkus: ProductsSkus
-
-    private enum CodingKeys: String, CodingKey {
-        case productsSkus = "data"
-    }
-}
+typealias ProductsSKUs = [[String: String]]
diff --git a/Networking/Networking/Mapper/ProductTagListMapper.swift b/Networking/Networking/Mapper/ProductTagListMapper.swift
index 359f164c4d2..d5f29fbf33a 100644
--- a/Networking/Networking/Mapper/ProductTagListMapper.swift
+++ b/Networking/Networking/Mapper/ProductTagListMapper.swift
@@ -22,20 +22,16 @@ struct ProductTagListMapper: Mapper {
 
         switch responseType {
         case .load:
+            return try extract(from: response, siteID: siteID)
+        case .create:
+            let container: ProductTagListBatchCreateContainer
             if hasDataEnvelope {
-                return try decoder.decode(ProductTagListEnvelope.self, from: response).tags
+                container = try decoder.decode(Envelope<ProductTagListBatchCreateContainer>.self, from: response).data
             } else {
-                return try decoder.decode([ProductTag].self, from: response)
+                container = try decoder.decode(ProductTagListBatchCreateContainer.self, from: response)
             }
-        case .create:
-            let tags: [ProductTagFromBatchCreation] = try {
-                if hasDataEnvelope {
-                    return try decoder.decode(ProductTagListBatchCreateEnvelope.self, from: response).data.tags
-                } else {
-                    return try decoder.decode(ProductTagListBatchCreateContainer.self, from: response).tags
-                }
-            }()
-            return tags
+
+            return container.tags
                 .filter { $0.error == nil }
                 .compactMap { (tagCreated) -> ProductTag? in
                     if let name = tagCreated.name, let slug = tagCreated.slug {
@@ -45,11 +41,14 @@ struct ProductTagListMapper: Mapper {
                 }
 
         case .delete:
+            let container: ProductTagListBatchDeleteContainer
             if hasDataEnvelope {
-                return try decoder.decode(ProductTagListBatchDeleteEnvelope.self, from: response).data.tags
+                container = try decoder.decode(Envelope<ProductTagListBatchDeleteContainer>.self, from: response).data
             } else {
-                return try decoder.decode(ProductTagListBatchDeleteContainer.self, from: response).tags
+                container = try decoder.decode(ProductTagListBatchDeleteContainer.self, from: response)
             }
+
+            return container.tags
         }
     }
 
@@ -60,32 +59,6 @@ struct ProductTagListMapper: Mapper {
     }
 }
 
-
-/// ProductTagListEnvelope Disposable Entity:
-/// `Load All Products Tags` endpoint returns the products tags in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductTagListEnvelope: Decodable {
-    let tags: [ProductTag]
-
-    private enum CodingKeys: String, CodingKey {
-        case tags = "data"
-    }
-}
-
-
-/// ProductTagListBatchCreateEnvelope Disposable Entity:
-/// `Batch Create Products Tags` endpoint returns the products tags under the `data` key, nested under `create`  key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductTagListBatchCreateEnvelope: Decodable {
-    let data: ProductTagListBatchCreateContainer
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
-
 private struct ProductTagListBatchCreateContainer: Decodable {
     let tags: [ProductTagFromBatchCreation]
 
@@ -94,18 +67,6 @@ private struct ProductTagListBatchCreateContainer: Decodable {
     }
 }
 
-/// ProductTagListBatchDeleteEnvelope Disposable Entity:
-/// `Batch Delete Products Tags` endpoint returns the products tags under the `data` key, nested under `delete` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductTagListBatchDeleteEnvelope: Decodable {
-    let data: ProductTagListBatchDeleteContainer
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
-
 private struct ProductTagListBatchDeleteContainer: Decodable {
     let tags: [ProductTag]
 
diff --git a/Networking/Networking/Mapper/ProductVariationListMapper.swift b/Networking/Networking/Mapper/ProductVariationListMapper.swift
index 08aaf486834..af9c642d72c 100644
--- a/Networking/Networking/Mapper/ProductVariationListMapper.swift
+++ b/Networking/Networking/Mapper/ProductVariationListMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: ProductVariation List
 ///
 struct ProductVariationListMapper: Mapper {
@@ -26,23 +23,6 @@ struct ProductVariationListMapper: Mapper {
             .productID: productID
         ]
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductVariationsEnvelope.self, from: response).productVariations
-        } else {
-            return try decoder.decode([ProductVariation].self, from: response)
-        }
-    }
-}
-
-/// ProductVariationsEnvelope Disposable Entity
-///
-/// `Load Product Variations` endpoint returns the requested objects in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct ProductVariationsEnvelope: Decodable {
-    let productVariations: [ProductVariation]
-
-    private enum CodingKeys: String, CodingKey {
-        case productVariations = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/ProductVariationMapper.swift b/Networking/Networking/Mapper/ProductVariationMapper.swift
index 11537863dc0..f33b2aed5fa 100644
--- a/Networking/Networking/Mapper/ProductVariationMapper.swift
+++ b/Networking/Networking/Mapper/ProductVariationMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: ProductVariation
 ///
 struct ProductVariationMapper: Mapper {
@@ -25,23 +23,6 @@ struct ProductVariationMapper: Mapper {
             .productID: productID
         ]
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductVariationEnvelope.self, from: response).productVariation
-        } else {
-            return try decoder.decode(ProductVariation.self, from: response)
-        }
-    }
-}
-
-/// ProductVariationEnvelope Disposable Entity
-///
-/// `ProductVariation` endpoint returns the requested product variation document in the `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductVariationEnvelope: Decodable {
-    let productVariation: ProductVariation
-
-    private enum CodingKeys: String, CodingKey {
-        case productVariation = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/ProductVariationsBulkCreateMapper.swift b/Networking/Networking/Mapper/ProductVariationsBulkCreateMapper.swift
index d92643db1db..9143867b097 100644
--- a/Networking/Networking/Mapper/ProductVariationsBulkCreateMapper.swift
+++ b/Networking/Networking/Mapper/ProductVariationsBulkCreateMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: ProductVariationsBulkCreateMapper
 ///
 struct ProductVariationsBulkCreateMapper: Mapper {
@@ -24,24 +22,15 @@ struct ProductVariationsBulkCreateMapper: Mapper {
             .siteID: siteID,
             .productID: productID
         ]
+
+        let container: ProductVariationsContainer
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductVariationsContainerEnvelope.self, from: response).data.createdProductVariations
+            container = try decoder.decode(Envelope<ProductVariationsContainer>.self, from: response).data
         } else {
-            return try decoder.decode(ProductVariationsContainer.self, from: response).createdProductVariations
+            container = try decoder.decode(ProductVariationsContainer.self, from: response)
         }
-    }
-}
 
-/// ProductVariationsEnvelope Disposable Entity
-///
-/// `Variations/batch` endpoint returns the requested create product variations document in a `create` key, nested in a `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductVariationsContainerEnvelope: Decodable {
-    let data: ProductVariationsContainer
-
-    private enum CodingKeys: String, CodingKey {
-        case data
+        return container.createdProductVariations
     }
 }
 
diff --git a/Networking/Networking/Mapper/ProductVariationsBulkUpdateMapper.swift b/Networking/Networking/Mapper/ProductVariationsBulkUpdateMapper.swift
index f7283f6f18c..08948d297d3 100644
--- a/Networking/Networking/Mapper/ProductVariationsBulkUpdateMapper.swift
+++ b/Networking/Networking/Mapper/ProductVariationsBulkUpdateMapper.swift
@@ -24,24 +24,15 @@ struct ProductVariationsBulkUpdateMapper: Mapper {
             .siteID: siteID,
             .productID: productID
         ]
+
+        let container: ProductVariationsContainer
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductVariationsContainerEnvelope.self, from: response).data.updatedProductVariations
+            container = try decoder.decode(Envelope<ProductVariationsContainer>.self, from: response).data
         } else {
-            return try decoder.decode(ProductVariationsContainer.self, from: response).updatedProductVariations
+            container = try decoder.decode(ProductVariationsContainer.self, from: response)
         }
-    }
-}
 
-/// ProductVariationsEnvelope Disposable Entity
-///
-/// `Variations/batch` endpoint returns the requested update product variations document in a `update` key, nested in a `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductVariationsContainerEnvelope: Decodable {
-    let data: ProductVariationsContainer
-
-    private enum CodingKeys: String, CodingKey {
-        case data
+        return container.updatedProductVariations
     }
 }
 
diff --git a/Networking/Networking/Mapper/ProductsBulkUpdateMapper.swift b/Networking/Networking/Mapper/ProductsBulkUpdateMapper.swift
index b883e89f1b6..249596d811e 100644
--- a/Networking/Networking/Mapper/ProductsBulkUpdateMapper.swift
+++ b/Networking/Networking/Mapper/ProductsBulkUpdateMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: ProductsBulkUpdateMapper
 ///
 struct ProductsBulkUpdateMapper: Mapper {
@@ -17,27 +15,15 @@ struct ProductsBulkUpdateMapper: Mapper {
         decoder.userInfo = [
             .siteID: siteID
         ]
+
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductsContainerEnvelope.self, from: response).data.updatedProducts
+            return try decoder.decode(Envelope<ProductsContainer>.self, from: response).data.updatedProducts
         } else {
             return try decoder.decode(ProductsContainer.self, from: response).updatedProducts
         }
     }
 }
 
-/// ProductsEnvelope Disposable Entity
-///
-/// `products/batch` endpoint returns the requested updated products in a `update` key, nested in a `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ProductsContainerEnvelope: Decodable {
-    let data: ProductsContainer
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
-
 private struct ProductsContainer: Decodable {
     let updatedProducts: [Product]
 
diff --git a/Networking/Networking/Mapper/ProductsReportMapper.swift b/Networking/Networking/Mapper/ProductsReportMapper.swift
index f41fb613fe2..be63b10d5d5 100644
--- a/Networking/Networking/Mapper/ProductsReportMapper.swift
+++ b/Networking/Networking/Mapper/ProductsReportMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: `[ProductsReportItem]`
 ///
 struct ProductsReportMapper: Mapper {
@@ -9,22 +7,9 @@ struct ProductsReportMapper: Mapper {
     func map(response: Data) throws -> [ProductsReportItem] {
         let decoder = JSONDecoder()
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ProductsReportEnvelope.self, from: response).items
+            return try decoder.decode(Envelope<[ProductsReportItem]>.self, from: response).data
         } else {
             return try decoder.decode([ProductsReportItem].self, from: response)
         }
     }
 }
-
-
-/// ProductsReportEnvelope Disposable Entity:
-/// Load Products Report endpoint returns the coupon in the `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct ProductsReportEnvelope: Decodable {
-    let items: [ProductsReportItem]
-
-    private enum CodingKeys: String, CodingKey {
-        case items = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/ProductsTotalMapper.swift b/Networking/Networking/Mapper/ProductsTotalMapper.swift
index 3d01f5509fe..23311e0c936 100644
--- a/Networking/Networking/Mapper/ProductsTotalMapper.swift
+++ b/Networking/Networking/Mapper/ProductsTotalMapper.swift
@@ -1,19 +1,16 @@
-import Foundation
-
 /// Mapper: ProductsTotal
 ///
 struct ProductsTotalMapper: Mapper {
     func map(response: Data) throws -> Int64 {
         let decoder = JSONDecoder()
-        let totals: [ProductTypeTotal]
 
+        let totals: [ProductTypeTotal]
         if hasDataEnvelope(in: response) {
-            totals = try decoder
-                .decode(ProductTypeTotalListEnvelope.self, from: response)
-                .totals
+            totals = try decoder .decode(Envelope<[ProductTypeTotal]>.self, from: response).data
         } else {
             totals = try decoder.decode([ProductTypeTotal].self, from: response)
         }
+
         return totals.map { $0.total }.reduce(0, +)
     }
 }
@@ -25,11 +22,3 @@ private struct ProductTypeTotal: Decodable {
         case total
     }
 }
-
-private struct ProductTypeTotalListEnvelope: Decodable {
-    let totals: [ProductTypeTotal]
-
-    private enum CodingKeys: String, CodingKey {
-        case totals = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/ReaderConnectionTokenMapper.swift b/Networking/Networking/Mapper/ReaderConnectionTokenMapper.swift
index a3e9eb8d491..2a1436c8c87 100644
--- a/Networking/Networking/Mapper/ReaderConnectionTokenMapper.swift
+++ b/Networking/Networking/Mapper/ReaderConnectionTokenMapper.swift
@@ -1,30 +1,8 @@
-/// Mapper: Card reader connection token
-///
 struct ReaderConnectionTokenMapper: Mapper {
 
     /// (Attempts) to convert a dictionary into a connection token.
     ///
     func map(response: Data) throws -> ReaderConnectionToken {
-        let decoder = JSONDecoder()
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ReaderConnectionTokenEnvelope.self, from: response).token
-        } else {
-            return try decoder.decode(ReaderConnectionToken.self, from: response)
-        }
-    }
-}
-
-
-/// ReaderConnectionTokenEnvelope Disposable Entity
-///
-/// `Load connection Token` endpoint returns the requested connection token and test mode in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct ReaderConnectionTokenEnvelope: Decodable {
-    let token: ReaderConnectionToken
-
-    private enum CodingKeys: String, CodingKey {
-        case token = "data"
+        return try extract(from: response)
     }
 }
diff --git a/Networking/Networking/Mapper/RefundListMapper.swift b/Networking/Networking/Mapper/RefundListMapper.swift
index 75c78d69b6f..6ed4803b426 100644
--- a/Networking/Networking/Mapper/RefundListMapper.swift
+++ b/Networking/Networking/Mapper/RefundListMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Refund List
 ///
 struct RefundListMapper: Mapper {
@@ -16,7 +13,6 @@ struct RefundListMapper: Mapper {
     ///
     let orderID: Int64
 
-
     /// (Attempts) to convert a dictionary into [Refund].
     ///
     func map(response: Data) throws -> [Refund] {
@@ -27,24 +23,6 @@ struct RefundListMapper: Mapper {
             .orderID: orderID
         ]
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(RefundsEnvelope.self, from: response).refunds
-        } else {
-            return try decoder.decode([Refund].self, from: response)
-        }
-    }
-}
-
-
-/// RefundsEnvelope Disposable Entity
-///
-/// `Load Refunds` endpoint returns the requested order refunds document in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct RefundsEnvelope: Decodable {
-    let refunds: [Refund]
-
-    private enum CodingKeys: String, CodingKey {
-        case refunds = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/RefundMapper.swift b/Networking/Networking/Mapper/RefundMapper.swift
index f9b1f52ca8f..72479ce0b9e 100644
--- a/Networking/Networking/Mapper/RefundMapper.swift
+++ b/Networking/Networking/Mapper/RefundMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Refund
 ///
 struct RefundMapper: Mapper {
@@ -16,7 +13,6 @@ struct RefundMapper: Mapper {
     ///
     let orderID: Int64
 
-
     /// (Attempts) to convert a dictionary into a single Refund.
     ///
     func map(response: Data) throws -> Refund {
@@ -28,7 +24,7 @@ struct RefundMapper: Mapper {
         ]
 
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(RefundEnvelope.self, from: response).refund
+            return try decoder.decode(Envelope<Refund>.self, from: response).data
         } else {
             return try decoder.decode(Refund.self, from: response)
         }
@@ -42,17 +38,3 @@ struct RefundMapper: Mapper {
         return try encoder.encode(refund)
     }
 }
-
-
-/// RefundEnvelope Disposable Entity
-///
-/// `Load Refund` endpoint returns the requested order refund document in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct RefundEnvelope: Decodable {
-    let refund: Refund
-
-    private enum CodingKeys: String, CodingKey {
-        case refund = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/RemotePaymentIntentMapper.swift b/Networking/Networking/Mapper/RemotePaymentIntentMapper.swift
index 8b1282a16c1..c0cf14ce862 100644
--- a/Networking/Networking/Mapper/RemotePaymentIntentMapper.swift
+++ b/Networking/Networking/Mapper/RemotePaymentIntentMapper.swift
@@ -1,31 +1,8 @@
-import Foundation
-
-/// Mapper: WCPay Payment Intent
-///
 struct RemotePaymentIntentMapper: Mapper {
 
     /// (Attempts) to convert a dictionary into an payment intent.
     ///
     func map(response: Data) throws -> RemotePaymentIntent {
-        let decoder = JSONDecoder()
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(WCPayPaymentIntentEnvelope.self, from: response).paymentIntent
-        } else {
-            return try decoder.decode(RemotePaymentIntent.self, from: response)
-        }
-    }
-}
-
-/// WCPayPaymentIntentEnvelope Disposable Entity
-///
-/// Endpoint returns the payment intent in the `data` key. This entity
-/// allows us to parse it with JSONDecoder.
-///
-private struct WCPayPaymentIntentEnvelope: Decodable {
-    let paymentIntent: RemotePaymentIntent
-
-    private enum CodingKeys: String, CodingKey {
-        case paymentIntent = "data"
+        return try extract(from: response)
     }
 }
diff --git a/Networking/Networking/Mapper/RemoteReaderLocationMapper.swift b/Networking/Networking/Mapper/RemoteReaderLocationMapper.swift
index d6c1bca8f74..3576ab52148 100644
--- a/Networking/Networking/Mapper/RemoteReaderLocationMapper.swift
+++ b/Networking/Networking/Mapper/RemoteReaderLocationMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: WCPay Reader Location
 ///
 struct RemoteReaderLocationMapper: Mapper {
@@ -9,23 +7,6 @@ struct RemoteReaderLocationMapper: Mapper {
     func map(response: Data) throws -> RemoteReaderLocation {
         let decoder = JSONDecoder()
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(RemoteReaderLocationEnvelope.self, from: response).location
-        } else {
-            return try decoder.decode(RemoteReaderLocation.self, from: response)
-        }
-    }
-}
-
-/// WCPayLocationEnvelope Disposable Entity
-///
-/// Endpoint returns the location in the `data` key. This entity
-/// allows us to parse it with JSONDecoder.
-///
-private struct RemoteReaderLocationEnvelope: Decodable {
-    let location: RemoteReaderLocation
-
-    private enum CodingKeys: String, CodingKey {
-        case location = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/ReportOrderTotalsMapper.swift b/Networking/Networking/Mapper/ReportOrderTotalsMapper.swift
index e2170f028c2..e2c8f9cabf0 100644
--- a/Networking/Networking/Mapper/ReportOrderTotalsMapper.swift
+++ b/Networking/Networking/Mapper/ReportOrderTotalsMapper.swift
@@ -1,39 +1 @@
-import Foundation
-
-
-/// Mapper: Order totals report
-///
-struct ReportOrderTotalsMapper: Mapper {
-
-    /// Site Identifier associated to the settings that will be parsed.
-    /// We're injecting this field via `JSONDecoder.userInfo` because
-    /// the remote endpoints don't really return the SiteID in any of the
-    /// settings endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to extract order totals report from a given JSON Encoded response.
-    ///
-    func map(response: Data) throws -> [OrderStatus] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ReportOrderTotalsEnvelope.self, from: response).data
-        } else {
-            return try decoder.decode([OrderStatus].self, from: response)
-        }
-    }
-}
-
-/// The report endpoint returns the totals document within a `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct ReportOrderTotalsEnvelope: Decodable {
-    let data: [OrderStatus]
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
+typealias ReportOrderTotalsMapper = SiteIDMapper<[OrderStatus]>
diff --git a/Networking/Networking/Mapper/ShipmentTrackingListMapper.swift b/Networking/Networking/Mapper/ShipmentTrackingListMapper.swift
index 101064297ac..e3edfbd8691 100644
--- a/Networking/Networking/Mapper/ShipmentTrackingListMapper.swift
+++ b/Networking/Networking/Mapper/ShipmentTrackingListMapper.swift
@@ -1,6 +1,5 @@
 import Foundation
 
-
 /// Mapper for an array of `ShipmentTracking` JSON objects
 ///
 struct ShipmentTrackingListMapper: Mapper {
@@ -28,22 +27,9 @@ struct ShipmentTrackingListMapper: Mapper {
         ]
 
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShipmentTrackingListEnvelope.self, from: response).shipmentTrackings
+            return try decoder.decode(Envelope<[ShipmentTracking]>.self, from: response).data
         } else {
             return try decoder.decode([ShipmentTracking].self, from: response)
         }
     }
 }
-
-
-/// ShipmentTracking list disposable entity:
-/// `Load Shipment Trackings` endpoint returns all of its tracking details within the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct ShipmentTrackingListEnvelope: Decodable {
-    let shipmentTrackings: [ShipmentTracking]
-
-    private enum CodingKeys: String, CodingKey {
-        case shipmentTrackings = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/ShipmentTrackingProviderListMapper.swift b/Networking/Networking/Mapper/ShipmentTrackingProviderListMapper.swift
index 9583fd46969..e54af99e128 100644
--- a/Networking/Networking/Mapper/ShipmentTrackingProviderListMapper.swift
+++ b/Networking/Networking/Mapper/ShipmentTrackingProviderListMapper.swift
@@ -3,34 +3,23 @@ import Foundation
 /// (Attempts) to convert a dictionary into an ShipmentTrackingProviderGroup entity.
 ///
 struct ShipmentTrackingProviderListMapper: Mapper {
+
     private let siteID: Int64
 
     public init(siteID: Int64) {
         self.siteID = siteID
     }
 
+    private typealias RawData = [String: [String: String]]
+
     func map(response: Data) throws -> [ShipmentTrackingProviderGroup] {
         let decoder = JSONDecoder()
-        let rawDictionary: ShipmentTrackingProviderListEnvelope.RawData
+        let rawDictionary: RawData
         if hasDataEnvelope(in: response) {
-            rawDictionary = try decoder.decode(ShipmentTrackingProviderListEnvelope.self, from: response).rawData
+            rawDictionary = try decoder.decode(Envelope<RawData>.self, from: response).data
         } else {
-            rawDictionary = try decoder.decode(ShipmentTrackingProviderListEnvelope.RawData.self, from: response)
+            rawDictionary = try decoder.decode(RawData.self, from: response)
         }
         return rawDictionary.map({ ShipmentTrackingProviderGroup(name: $0.key, siteID: siteID, dictionary: $0.value) })
     }
 }
-
-
-/// ShipmentTrackingProviderListEnvelope Disposable Entity: The shipment tracking provider endpoint returns
-/// the providers within a `data` key.
-///
-private struct ShipmentTrackingProviderListEnvelope: Decodable {
-    typealias RawData = [String: [String: String]]
-
-    let rawData: RawData
-
-    private enum CodingKeys: String, CodingKey {
-        case rawData = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/ShippingLabelAccountSettingsMapper.swift b/Networking/Networking/Mapper/ShippingLabelAccountSettingsMapper.swift
index b5e2c6397b5..50807634dd7 100644
--- a/Networking/Networking/Mapper/ShippingLabelAccountSettingsMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelAccountSettingsMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: Shipping Label Account Settings
 ///
 struct ShippingLabelAccountSettingsMapper: Mapper {
@@ -17,22 +15,6 @@ struct ShippingLabelAccountSettingsMapper: Mapper {
             .siteID: siteID
         ]
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShippingLabelAccountSettingsMapperEnvelope.self, from: response).data
-        } else {
-            return try decoder.decode(ShippingLabelAccountSettings.self, from: response)
-        }
-    }
-}
-
-/// ShippingLabelAccountSettingsMapperEnvelope Disposable Entity:
-/// `Shipping Label Account Settings` endpoint returns the shipping label account settings in the `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct ShippingLabelAccountSettingsMapperEnvelope: Decodable {
-    let data: ShippingLabelAccountSettings
-
-    private enum CodingKeys: String, CodingKey {
-        case data
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/ShippingLabelAddressValidationSuccessMapper.swift b/Networking/Networking/Mapper/ShippingLabelAddressValidationSuccessMapper.swift
index 402e980727a..92c9b7b0403 100644
--- a/Networking/Networking/Mapper/ShippingLabelAddressValidationSuccessMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelAddressValidationSuccessMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Shipping Label Address Validation Response
 ///
 struct ShippingLabelAddressValidationSuccessMapper: Mapper {
@@ -8,25 +5,14 @@ struct ShippingLabelAddressValidationSuccessMapper: Mapper {
     ///
     func map(response: Data) throws -> ShippingLabelAddressValidationSuccess {
         let decoder = JSONDecoder()
-        let data: ShippingLabelAddressValidationResponse = try {
-            if hasDataEnvelope(in: response) {
-                return try decoder.decode(ShippingLabelAddressValidationResponseEnvelope.self, from: response).data
-            } else {
-                return try decoder.decode(ShippingLabelAddressValidationResponse.self, from: response)
-            }
-        }()
-        return try data.result.get()
-    }
-}
 
-/// ShippingLabelAddressValidationResponseEnvelope Disposable Entity:
-/// `Normalize Address` endpoint returns the shipping label address document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ShippingLabelAddressValidationResponseEnvelope: Decodable {
-    let data: ShippingLabelAddressValidationResponse
+        let data: ShippingLabelAddressValidationResponse
+        if hasDataEnvelope(in: response) {
+            data = try decoder.decode(Envelope<ShippingLabelAddressValidationResponse>.self, from: response).data
+        } else {
+            data = try decoder.decode(ShippingLabelAddressValidationResponse.self, from: response)
+        }
 
-    private enum CodingKeys: String, CodingKey {
-        case data = "data"
+        return try data.result.get()
     }
 }
diff --git a/Networking/Networking/Mapper/ShippingLabelCarriersAndRatesMapper.swift b/Networking/Networking/Mapper/ShippingLabelCarriersAndRatesMapper.swift
index c0299c73c10..9c27134f8ea 100644
--- a/Networking/Networking/Mapper/ShippingLabelCarriersAndRatesMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelCarriersAndRatesMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Shipping Label Carriers and Rates Data
 ///
 struct ShippingLabelCarriersAndRatesMapper: Mapper {
@@ -8,23 +5,15 @@ struct ShippingLabelCarriersAndRatesMapper: Mapper {
     ///
     func map(response: Data) throws -> [ShippingLabelCarriersAndRates] {
         let decoder = JSONDecoder()
+
+        let container: ShippingLabelRatesEnvelope
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShippingLabelDataEnvelope.self, from: response).data.rates.boxes
+            container = try decoder.decode(Envelope<ShippingLabelRatesEnvelope>.self, from: response).data
         } else {
-            return try decoder.decode(ShippingLabelRatesEnvelope.self, from: response).rates.boxes
+            container = try decoder.decode(ShippingLabelRatesEnvelope.self, from: response)
         }
-    }
-}
-
-/// ShippingLabelDataEnvelope Disposable Entity:
-/// `Carriers and Rates Shipping Label` endpoint returns the shipping label document under `data` -> `rates` -> `default_box`  key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ShippingLabelDataEnvelope: Decodable {
-    let data: ShippingLabelRatesEnvelope
 
-    private enum CodingKeys: String, CodingKey {
-        case data
+        return container.rates.boxes
     }
 }
 
diff --git a/Networking/Networking/Mapper/ShippingLabelCreationEligibilityMapper.swift b/Networking/Networking/Mapper/ShippingLabelCreationEligibilityMapper.swift
index d3f9607f120..0ed32e2ea8c 100644
--- a/Networking/Networking/Mapper/ShippingLabelCreationEligibilityMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelCreationEligibilityMapper.swift
@@ -1,28 +1,7 @@
-import Foundation
-
-/// Mapper: Shipping Label Creation Eligibility
-///
 struct ShippingLabelCreationEligibilityMapper: Mapper {
     /// (Attempts) to convert a dictionary into ShippingLabelAccountSettings.
     ///
     func map(response: Data) throws -> ShippingLabelCreationEligibilityResponse {
-        let decoder = JSONDecoder()
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShippingLabelCreationEligibilityMapperEnvelope.self, from: response).eligibility
-        } else {
-            return try decoder.decode(ShippingLabelCreationEligibilityResponse.self, from: response)
-        }
-    }
-}
-
-/// ShippingLabelCreationEligibilityMapperEnvelope Disposable Entity:
-/// `Shipping Label Creation Eligibility` endpoint returns the shipping label account settings in the `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct ShippingLabelCreationEligibilityMapperEnvelope: Decodable {
-    let eligibility: ShippingLabelCreationEligibilityResponse
-
-    private enum CodingKeys: String, CodingKey {
-        case eligibility = "data"
+        try extract(from: response)
     }
 }
diff --git a/Networking/Networking/Mapper/ShippingLabelPackagesMapper.swift b/Networking/Networking/Mapper/ShippingLabelPackagesMapper.swift
index f8cc2c78fd5..e29df0a8b52 100644
--- a/Networking/Networking/Mapper/ShippingLabelPackagesMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelPackagesMapper.swift
@@ -1,29 +1,7 @@
-import Foundation
-
-
-/// Mapper: Shipping Label Packages
-///
 struct ShippingLabelPackagesMapper: Mapper {
     /// (Attempts) to convert a dictionary into ShippingLabelPackagesResponse.
     ///
     func map(response: Data) throws -> ShippingLabelPackagesResponse {
-        let decoder = JSONDecoder()
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShippingLabelPackagesMapperEnvelope.self, from: response).data
-        } else {
-            return try decoder.decode(ShippingLabelPackagesResponse.self, from: response)
-        }
-    }
-}
-
-/// ShippingLabelPackagesMapperEnvelope Disposable Entity:
-/// `Shipping Label Packages` endpoint returns the shipping label packages in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ShippingLabelPackagesMapperEnvelope: Decodable {
-    let data: ShippingLabelPackagesResponse
-
-    private enum CodingKeys: String, CodingKey {
-        case data = "data"
+        try extract(from: response)
     }
 }
diff --git a/Networking/Networking/Mapper/ShippingLabelPrintDataMapper.swift b/Networking/Networking/Mapper/ShippingLabelPrintDataMapper.swift
index 56a055943af..b2e1e3bf0b9 100644
--- a/Networking/Networking/Mapper/ShippingLabelPrintDataMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelPrintDataMapper.swift
@@ -1,29 +1,7 @@
-import Foundation
-
-
-/// Mapper: Shipping Label Print Data
-///
 struct ShippingLabelPrintDataMapper: Mapper {
     /// (Attempts) to convert a dictionary into ShippingLabelPrintData.
     ///
     func map(response: Data) throws -> ShippingLabelPrintData {
-        let decoder = JSONDecoder()
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShippingLabelPrintDataEnvelope.self, from: response).printData
-        } else {
-            return try decoder.decode(ShippingLabelPrintData.self, from: response)
-        }
-    }
-}
-
-/// ShippingLabelPrintDataEnvelope Disposable Entity:
-/// `Print Shipping Label` endpoint returns the shipping label document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ShippingLabelPrintDataEnvelope: Decodable {
-    let printData: ShippingLabelPrintData
-
-    private enum CodingKeys: String, CodingKey {
-        case printData = "data"
+        try extract(from: response)
     }
 }
diff --git a/Networking/Networking/Mapper/ShippingLabelPurchaseMapper.swift b/Networking/Networking/Mapper/ShippingLabelPurchaseMapper.swift
index 770b547d426..495f50a6998 100644
--- a/Networking/Networking/Mapper/ShippingLabelPurchaseMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelPurchaseMapper.swift
@@ -25,23 +25,14 @@ struct ShippingLabelPurchaseMapper: Mapper {
             .orderID: orderID
         ]
 
+        let container: ShippingLabelPurchaseEnvelope
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShippingLabelPurchaseResponse.self, from: response).data.labels
+            container = try decoder.decode(Envelope<ShippingLabelPurchaseEnvelope>.self, from: response).data
         } else {
-            return try decoder.decode(ShippingLabelPurchaseEnvelope.self, from: response).labels
+            container = try decoder.decode(ShippingLabelPurchaseEnvelope.self, from: response)
         }
-    }
-}
-
-/// ShippingLabelPurchaseResponse Disposable Entity
-///
-/// `Purchase Shipping Labels` endpoint returns the data wrapper in the `data` key.
-///
-private struct ShippingLabelPurchaseResponse: Decodable {
-    let data: ShippingLabelPurchaseEnvelope
 
-    private enum CodingKeys: String, CodingKey {
-        case data
+        return container.labels
     }
 }
 
diff --git a/Networking/Networking/Mapper/ShippingLabelRefundMapper.swift b/Networking/Networking/Mapper/ShippingLabelRefundMapper.swift
index a3fe963d31a..216f918cfc0 100644
--- a/Networking/Networking/Mapper/ShippingLabelRefundMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelRefundMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Shipping Label Refund Mapper
 ///
 struct ShippingLabelRefundMapper: Mapper {
@@ -10,25 +7,13 @@ struct ShippingLabelRefundMapper: Mapper {
         let decoder = JSONDecoder()
         decoder.dateDecodingStrategy = .millisecondsSince1970
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShippingLabelRefundResponse.self, from: response).data.refund
+            return try decoder.decode(Envelope<ShippingLabelRefundEnvelope>.self, from: response).data.refund
         } else {
             return try decoder.decode(ShippingLabelRefundEnvelope.self, from: response).refund
         }
     }
 }
 
-/// ShippingLabelRefundResponse Disposable Entity:
-/// `Refund Shipping Label` endpoint returns the refund data wrapper in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct ShippingLabelRefundResponse: Decodable {
-    let data: ShippingLabelRefundEnvelope
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
-
 /// ShippingLabelRefundEnvelope Disposable Entity:
 /// `Refund Shipping Label` endpoint returns the refund in the `data.refund` key.
 /// This entity allows us to do parse all the things with JSONDecoder.
diff --git a/Networking/Networking/Mapper/ShippingLabelStatusMapper.swift b/Networking/Networking/Mapper/ShippingLabelStatusMapper.swift
index ad6444a131a..25a1715159f 100644
--- a/Networking/Networking/Mapper/ShippingLabelStatusMapper.swift
+++ b/Networking/Networking/Mapper/ShippingLabelStatusMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: Check Status of Shipping Labels
 ///
 struct ShippingLabelStatusMapper: Mapper {
@@ -26,25 +24,13 @@ struct ShippingLabelStatusMapper: Mapper {
         ]
 
         if hasDataEnvelope(in: response) {
-            return try decoder.decode(ShippingLabelStatusResponse.self, from: response).data.labels
+            return try decoder.decode(Envelope<ShippingLabelStatusEnvelope>.self, from: response).data.labels
         } else {
             return try decoder.decode(ShippingLabelStatusEnvelope.self, from: response).labels
         }
     }
 }
 
-/// ShippingLabelPurchaseResponse Disposable Entity
-///
-/// `Check Shipping Labels Status` endpoint returns the data wrapper in the `data` key.
-///
-private struct ShippingLabelStatusResponse: Decodable {
-    let data: ShippingLabelStatusEnvelope
-
-    private enum CodingKeys: String, CodingKey {
-        case data
-    }
-}
-
 /// ShippingLabelPurchaseEnvelope Disposable Entity
 ///
 /// `Check Shipping Labels Status` endpoint returns the shipping label purchases in the `data.labels` key.
diff --git a/Networking/Networking/Mapper/SiteAPIMapper.swift b/Networking/Networking/Mapper/SiteAPIMapper.swift
index e1403b127ca..888918dfe66 100644
--- a/Networking/Networking/Mapper/SiteAPIMapper.swift
+++ b/Networking/Networking/Mapper/SiteAPIMapper.swift
@@ -1,41 +1 @@
-import Foundation
-
-
-/// Mapper: SiteAPI
-///
-struct SiteAPIMapper: Mapper {
-
-    /// Site Identifier associated to the API information that will be parsed.
-    /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't return the SiteID.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [SiteSetting].
-    ///
-    func map(response: Data) throws -> SiteAPI {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(SiteAPIEnvelope.self, from: response).siteAPI
-        } else {
-            return try decoder.decode(SiteAPI.self, from: response)
-        }
-    }
-}
-
-
-/// SiteAPIEnvelope Disposable Entity:
-/// The settings endpoint returns the settings document within a `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct SiteAPIEnvelope: Decodable {
-    let siteAPI: SiteAPI
-
-    private enum CodingKeys: String, CodingKey {
-        case siteAPI = "data"
-    }
-}
+typealias SiteAPIMapper = SiteIDMapper<SiteAPI>
diff --git a/Networking/Networking/Mapper/SitePluginMapper.swift b/Networking/Networking/Mapper/SitePluginMapper.swift
index 0ed9cc34866..6949c976b96 100644
--- a/Networking/Networking/Mapper/SitePluginMapper.swift
+++ b/Networking/Networking/Mapper/SitePluginMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: SitePlugin
 ///
 struct SitePluginMapper: Mapper {
@@ -20,28 +18,6 @@ struct SitePluginMapper: Mapper {
     /// (Attempts) to convert a dictionary into SitePlugin.
     ///
     func map(response: Data) throws -> SitePlugin {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(SitePluginEnvelope.self, from: response).plugin
-        } else {
-            return try decoder.decode(SitePlugin.self, from: response)
-        }
-    }
-}
-
-
-/// SitePluginEnvelope Disposable Entity:
-/// The plugins endpoint returns the document within a `data` key. This entity
-/// allows us to do parse the returned plugin model with JSONDecoder.
-///
-private struct SitePluginEnvelope: Decodable {
-    let plugin: SitePlugin
-
-    private enum CodingKeys: String, CodingKey {
-        case plugin = "data"
+        try extract(from: response, siteID: siteID)
     }
 }
diff --git a/Networking/Networking/Mapper/SitePluginsMapper.swift b/Networking/Networking/Mapper/SitePluginsMapper.swift
index 78c3b6c71af..72a7b4bbebc 100644
--- a/Networking/Networking/Mapper/SitePluginsMapper.swift
+++ b/Networking/Networking/Mapper/SitePluginsMapper.swift
@@ -1,39 +1 @@
-import Foundation
-
-/// Mapper: SitePlugins
-///
-struct SitePluginsMapper: Mapper {
-
-    /// Site Identifier associated to the plugins that will be parsed.
-    /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't return the SiteID in the plugin endpoint.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [SitePlugin].
-    ///
-    func map(response: Data) throws -> [SitePlugin] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(SitePluginsEnvelope.self, from: response).plugins
-        } else {
-            return try decoder.decode([SitePlugin].self, from: response)
-        }
-    }
-}
-
-
-/// SitePluginsEnvelope Disposable Entity:
-/// The plugins endpoint returns the document within a `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct SitePluginsEnvelope: Decodable {
-    let plugins: [SitePlugin]
-
-    private enum CodingKeys: String, CodingKey {
-        case plugins = "data"
-    }
-}
+typealias SitePluginsMapper = SiteIDMapper<[SitePlugin]>
diff --git a/Networking/Networking/Mapper/SiteSettingMapper.swift b/Networking/Networking/Mapper/SiteSettingMapper.swift
index 3266cd1ecee..00579546f93 100644
--- a/Networking/Networking/Mapper/SiteSettingMapper.swift
+++ b/Networking/Networking/Mapper/SiteSettingMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper for a single SiteSetting
 ///
 struct SiteSettingMapper: Mapper {
@@ -25,23 +23,6 @@ struct SiteSettingMapper: Mapper {
             .settingGroupKey: settingsGroup.rawValue
         ]
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(SiteSettingEnvelope.self, from: response).setting
-        } else {
-            return try decoder.decode(SiteSetting.self, from: response)
-        }
-    }
-}
-
-
-/// SiteSettingEnvelope Disposable Entity:
-/// The plugins endpoint returns the document within a `data` key. This entity
-/// allows us to do parse the returned plugin model with JSONDecoder.
-///
-private struct SiteSettingEnvelope: Decodable {
-    let setting: SiteSetting
-
-    private enum CodingKeys: String, CodingKey {
-        case setting = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/SiteSettingsMapper.swift b/Networking/Networking/Mapper/SiteSettingsMapper.swift
index 57383d87b80..69cc6361916 100644
--- a/Networking/Networking/Mapper/SiteSettingsMapper.swift
+++ b/Networking/Networking/Mapper/SiteSettingsMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: SiteSettings
 ///
 struct SiteSettingsMapper: Mapper {
@@ -26,23 +23,6 @@ struct SiteSettingsMapper: Mapper {
             .settingGroupKey: settingsGroup.rawValue
         ]
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(SiteSettingsEnvelope.self, from: response).settings
-        } else {
-            return try decoder.decode([SiteSetting].self, from: response)
-        }
-    }
-}
-
-
-/// SiteSettingsEnvelope Disposable Entity:
-/// The settings endpoint returns the settings document within a `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct SiteSettingsEnvelope: Decodable {
-    let settings: [SiteSetting]
-
-    private enum CodingKeys: String, CodingKey {
-        case settings = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/StoreOnboardingTaskListMapper.swift b/Networking/Networking/Mapper/StoreOnboardingTaskListMapper.swift
index e5bf92d7e3b..0facc64dd50 100644
--- a/Networking/Networking/Mapper/StoreOnboardingTaskListMapper.swift
+++ b/Networking/Networking/Mapper/StoreOnboardingTaskListMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: StoreOnboardingTask
 ///
 struct StoreOnboardingTaskListMapper: Mapper {
@@ -9,8 +7,8 @@ struct StoreOnboardingTaskListMapper: Mapper {
 
         if hasDataEnvelope(in: response) {
             taskGroup = try decoder
-                .decode(StoreOnboardingTaskEnvelope.self, from: response)
-                .group
+                .decode(Envelope<[StoreOnboardingTaskGroup]>.self, from: response)
+                .data
         } else {
             taskGroup = try decoder.decode([StoreOnboardingTaskGroup].self, from: response)
         }
@@ -37,11 +35,3 @@ private struct StoreOnboardingTaskGroup: Decodable {
         case tasks
     }
 }
-
-private struct StoreOnboardingTaskEnvelope: Decodable {
-    let group: [StoreOnboardingTaskGroup]
-
-    private enum CodingKeys: String, CodingKey {
-        case group = "data"
-    }
-}
diff --git a/Networking/Networking/Mapper/StripeAccountMapper.swift b/Networking/Networking/Mapper/StripeAccountMapper.swift
index 04673fce20b..27a121d0bfc 100644
--- a/Networking/Networking/Mapper/StripeAccountMapper.swift
+++ b/Networking/Networking/Mapper/StripeAccountMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: Stripe Account
 ///
 struct StripeAccountMapper: Mapper {
@@ -9,28 +7,11 @@ struct StripeAccountMapper: Mapper {
     func map(response: Data) throws -> StripeAccount {
         let decoder = JSONDecoder()
 
-        /// Needed for currentDeadline, which is given as a UNIX timestamp.
-        /// Unfortunately other properties use other formats for dates, but we
-        /// can cross that bridge when we need those decoded.
+        // Needed for currentDeadline, which is given as a UNIX timestamp.
+        // Unfortunately other properties use other formats for dates, but we
+        // can cross that bridge when we need those decoded.
         decoder.dateDecodingStrategy = .secondsSince1970
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(StripeAccountEnvelope.self, from: response).account
-        } else {
-            return try decoder.decode(StripeAccount.self, from: response)
-        }
-    }
-}
-
-/// StripeAccountEnvelope Disposable Entity
-///
-/// Account endpoint returns the requested account in the `data` key. This entity
-/// allows us to parse it with JSONDecoder.
-///
-private struct StripeAccountEnvelope: Decodable {
-    let account: StripeAccount
-
-    private enum CodingKeys: String, CodingKey {
-        case account = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/SubscriptionListMapper.swift b/Networking/Networking/Mapper/SubscriptionListMapper.swift
index 3ed865c3be4..8ebcc4e8595 100644
--- a/Networking/Networking/Mapper/SubscriptionListMapper.swift
+++ b/Networking/Networking/Mapper/SubscriptionListMapper.swift
@@ -1,39 +1 @@
-import Foundation
-
-/// Mapper: `Subscription` List
-///
-struct SubscriptionListMapper: Mapper {
-    /// Site we're parsing `Subscription`s for
-    /// We're injecting this field by copying it in after parsing responses, because `siteID` is not returned in any of the Subscription endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into `[Subscription]`.
-    ///
-    func map(response: Data) throws -> [Subscription] {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(SubscriptionListEnvelope.self, from: response).subscriptions
-        } else {
-            return try decoder.decode([Subscription].self, from: response)
-        }
-    }
-}
-
-
-/// SubscriptionListEnvelope Disposable Entity:
-/// Load Subscriptions endpoint returns the subscriptions in the `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct SubscriptionListEnvelope: Decodable {
-    let subscriptions: [Subscription]
-
-    private enum CodingKeys: String, CodingKey {
-        case subscriptions = "data"
-    }
-}
+typealias SubscriptionListMapper = SiteIDMapper<[Subscription]>
diff --git a/Networking/Networking/Mapper/SubscriptionMapper.swift b/Networking/Networking/Mapper/SubscriptionMapper.swift
index 3db02705c76..6b38d57d6da 100644
--- a/Networking/Networking/Mapper/SubscriptionMapper.swift
+++ b/Networking/Networking/Mapper/SubscriptionMapper.swift
@@ -1,39 +1 @@
-import Foundation
-
-/// Mapper: `Subscription`
-///
-struct SubscriptionMapper: Mapper {
-    /// Site we're parsing `Subscription` for
-    /// We're injecting this field by copying it in after parsing responses, because `siteID` is not returned in any of the Subscription endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into `Subscription`.
-    ///
-    func map(response: Data) throws -> Subscription {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(SubscriptionEnvelope.self, from: response).subscription
-        } else {
-            return try decoder.decode(Subscription.self, from: response)
-        }
-    }
-}
-
-
-/// SubscriptionEnvelope Disposable Entity:
-/// Load Subscription endpoint returns the subscription in the `data` key.
-/// This entity allows us to parse all the things with JSONDecoder.
-///
-private struct SubscriptionEnvelope: Decodable {
-    let subscription: Subscription
-
-    private enum CodingKeys: String, CodingKey {
-        case subscription = "data"
-    }
-}
+typealias SubscriptionMapper = SiteIDMapper<Subscription>
diff --git a/Networking/Networking/Mapper/SuccessDataResultMapper.swift b/Networking/Networking/Mapper/SuccessDataResultMapper.swift
index a2f84fadb35..9d07484cb60 100644
--- a/Networking/Networking/Mapper/SuccessDataResultMapper.swift
+++ b/Networking/Networking/Mapper/SuccessDataResultMapper.swift
@@ -1,6 +1,3 @@
-import Foundation
-
-
 /// Mapper: Success Result Wrapped in `data` Key
 ///
 struct SuccessDataResultMapper: Mapper {
@@ -9,27 +6,16 @@ struct SuccessDataResultMapper: Mapper {
     ///
     func map(response: Data) throws -> Bool {
         let decoder = JSONDecoder()
-        let rawData: [String: Bool] = try {
-            if hasDataEnvelope(in: response) {
-                return try decoder.decode(SuccessDataResultEnvelope.self, from: response).rawData
-            } else {
-                return try decoder.decode([String: Bool].self, from: response)
-            }
-        }()
-        return rawData["success"] ?? false
+        let successResponse: SuccessResponse
+        if hasDataEnvelope(in: response) {
+            successResponse = try decoder.decode(Envelope<SuccessResponse>.self, from: response).data
+        } else {
+            successResponse = try decoder.decode(SuccessResponse.self, from: response)
+        }
+        return successResponse.success ?? false
     }
 }
 
-
-/// SuccessDataResultEnvelope Disposable Entity
-///
-/// Some endpoints return a "success" response in the `data` key. This entity
-/// allows us to parse that response with JSONDecoder.
-///
-private struct SuccessDataResultEnvelope: Decodable {
-    let rawData: [String: Bool]
-
-    private enum CodingKeys: String, CodingKey {
-        case rawData = "data"
-    }
+private struct SuccessResponse: Decodable {
+    let success: Bool?
 }
diff --git a/Networking/Networking/Mapper/SystemPluginMapper.swift b/Networking/Networking/Mapper/SystemPluginMapper.swift
index f4e426a2f21..f3f8e2b1cdb 100644
--- a/Networking/Networking/Mapper/SystemPluginMapper.swift
+++ b/Networking/Networking/Mapper/SystemPluginMapper.swift
@@ -19,7 +19,7 @@ struct SystemPluginMapper: Mapper {
 
         let systemStatus: SystemStatus = try {
             if hasDataEnvelope(in: response) {
-                return try decoder.decode(SystemStatusEnvelope.self, from: response).systemStatus
+                return try decoder.decode(Envelope<SystemStatus>.self, from: response).data
             } else {
                 return try decoder.decode(SystemStatus.self, from: response)
             }
diff --git a/Networking/Networking/Mapper/SystemStatusMapper.swift b/Networking/Networking/Mapper/SystemStatusMapper.swift
index 1145352e23f..939d1115542 100644
--- a/Networking/Networking/Mapper/SystemStatusMapper.swift
+++ b/Networking/Networking/Mapper/SystemStatusMapper.swift
@@ -1,37 +1 @@
-import Foundation
-
-/// Mapper: System Status
-///
-struct SystemStatusMapper: Mapper {
-
-    /// Site Identifier associated to the system status that will be parsed.
-    /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't return the SiteID in the system plugin endpoint.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into SystemStatus
-    ///
-    func map(response: Data) throws -> SystemStatus {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(SystemStatusEnvelope.self, from: response).systemStatus
-        } else {
-            return try decoder.decode(SystemStatus.self, from: response)
-        }
-    }
-}
-
-/// System Status endpoint returns the requested account in the `data` key. This entity
-/// allows us to parse it with JSONDecoder.
-///
-struct SystemStatusEnvelope: Decodable {
-    let systemStatus: SystemStatus
-
-    private enum CodingKeys: String, CodingKey {
-        case systemStatus = "data"
-    }
-}
+typealias SystemStatusMapper = SiteIDMapper<SystemStatus>
diff --git a/Networking/Networking/Mapper/TaxClassListMapper.swift b/Networking/Networking/Mapper/TaxClassListMapper.swift
index 555522d4e7a..8e7f9d9f51f 100644
--- a/Networking/Networking/Mapper/TaxClassListMapper.swift
+++ b/Networking/Networking/Mapper/TaxClassListMapper.swift
@@ -1,40 +1 @@
-import Foundation
-
-
-/// Mapper: TaxClass List
-///
-struct TaxClassListMapper: Mapper {
-
-    /// Site Identifier associated to the API information that will be parsed.
-    /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't return the SiteID.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [TaxClass].
-    ///
-    func map(response: Data) throws -> [TaxClass] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(TaxClassListEnvelope.self, from: response).taxClasses
-        } else {
-            return try decoder.decode([TaxClass].self, from: response)
-        }
-    }
-}
-
-
-/// TaxClassListEnvelope Disposable Entity:
-/// `Load All Tax Classes` endpoint returns the tax classes document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct TaxClassListEnvelope: Decodable {
-    let taxClasses: [TaxClass]
-
-    private enum CodingKeys: String, CodingKey {
-        case taxClasses = "data"
-    }
-}
+typealias TaxClassListMapper = SiteIDMapper<[TaxClass]>
diff --git a/Networking/Networking/Mapper/TaxRateListMapper.swift b/Networking/Networking/Mapper/TaxRateListMapper.swift
index 2033639212d..d8a4e49470f 100644
--- a/Networking/Networking/Mapper/TaxRateListMapper.swift
+++ b/Networking/Networking/Mapper/TaxRateListMapper.swift
@@ -1,38 +1 @@
-import Foundation
-
-/// Mapper: TaxRate List
-///
-struct TaxRateListMapper: Mapper {
-
-    /// Site Identifier associated to the API information that will be parsed.
-    /// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't return the SiteID.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into [TaxRate].
-    ///
-    func map(response: Data) throws -> [TaxRate] {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(TaxRateListEnvelope.self, from: response).taxClasses
-        } else {
-            return try decoder.decode([TaxRate].self, from: response)
-        }
-    }
-}
-
-/// TaxRateListEnvelope Disposable Entity:
-/// `Load All Tax Rates` endpoint returns the tax rates document in the `data` key.
-/// This entity allows us to do parse all the things with JSONDecoder.
-///
-private struct TaxRateListEnvelope: Decodable {
-    let taxClasses: [TaxRate]
-
-    private enum CodingKeys: String, CodingKey {
-        case taxClasses = "data"
-    }
-}
+typealias TaxRateListMapper = SiteIDMapper<[TaxRate]>
diff --git a/Networking/Networking/Mapper/TaxRateMapper.swift b/Networking/Networking/Mapper/TaxRateMapper.swift
index 9d76acca431..10232e85923 100644
--- a/Networking/Networking/Mapper/TaxRateMapper.swift
+++ b/Networking/Networking/Mapper/TaxRateMapper.swift
@@ -1,43 +1 @@
-import Foundation
-
-/// Mapper: TaxRate
-///
-struct TaxRateMapper: Mapper {
-
-    /// Site Identifier associated to the taxRate that will be parsed.
-    ///
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in any of the TaxRate Endpoints.
-    ///
-    let siteID: Int64
-
-
-    /// (Attempts) to convert a dictionary into TaxRate.
-    ///
-    func map(response: Data) throws -> TaxRate {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(TaxRateEnvelope.self, from: response).taxRate
-        } else {
-            return try decoder.decode(TaxRate.self, from: response)
-        }
-    }
-}
-
-
-/// TaxRate Envelope Disposable Entity
-///
-/// `Load TaxRate` endpoint returns the requested taxRate document in the `data` key. This entity
-/// allows us to do parse all the things with JSONDecoder.
-///
-private struct TaxRateEnvelope: Decodable {
-    let taxRate: TaxRate
-
-    private enum CodingKeys: String, CodingKey {
-        case taxRate = "data"
-    }
-}
+typealias TaxRateMapper = SiteIDMapper<TaxRate>
diff --git a/Networking/Networking/Mapper/UserMapper.swift b/Networking/Networking/Mapper/UserMapper.swift
index c5f032ec12b..3d3126efa41 100644
--- a/Networking/Networking/Mapper/UserMapper.swift
+++ b/Networking/Networking/Mapper/UserMapper.swift
@@ -1,38 +1 @@
-import Foundation
-
-/// Mapper: User
-///
-struct UserMapper: Mapper {
-    /// Site Identifier associated to the order that will be parsed.
-    /// We're injecting this field via `JSONDecoder.userInfo` because SiteID is not returned in the endpoints used to retrieve User models.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into User.
-    ///
-    func map(response: Data) throws -> User {
-        let decoder = JSONDecoder()
-        decoder.userInfo = [
-            .siteID: siteID
-        ]
-
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(UserEnvelope.self, from: response).user
-        } else {
-            return try decoder.decode(User.self, from: response)
-        }
-    }
-}
-
-/// UserEnvelope Disposable Entity
-///
-/// `Load User` endpoint returns the requested objects in the `data` key. This entity
-/// allows us to parse all the things with JSONDecoder.
-///
-private struct UserEnvelope: Decodable {
-    let user: User
-
-    private enum CodingKeys: String, CodingKey {
-        case user = "data"
-    }
-}
+typealias UserMapper = SiteIDMapper<User>
diff --git a/Networking/Networking/Mapper/WCAnalyticsCustomerMapper.swift b/Networking/Networking/Mapper/WCAnalyticsCustomerMapper.swift
index d744f033658..e8d227d765d 100644
--- a/Networking/Networking/Mapper/WCAnalyticsCustomerMapper.swift
+++ b/Networking/Networking/Mapper/WCAnalyticsCustomerMapper.swift
@@ -1,30 +1 @@
-import Foundation
-
-/// Mapper: WCAnalyticsCustomer
-///
-struct WCAnalyticsCustomerMapper: Mapper {
-    /// We're injecting this field by copying it in after parsing responses, because `siteID` is not returned in any of the Customer endpoints.
-    ///
-    let siteID: Int64
-
-    /// (Attempts) to convert a dictionary into a `[WCAnalyticsCustomer]` entity
-    ///
-    func map(response: Data) throws -> [WCAnalyticsCustomer] {
-        let decoder = JSONDecoder()
-        decoder.dateDecodingStrategy = .formatted(DateFormatter.Defaults.dateTimeFormatter)
-        decoder.userInfo = [.siteID: siteID]
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(WCAnalyticsCustomerEnvelope.self, from: response).customer
-        } else {
-            return try decoder.decode([WCAnalyticsCustomer].self, from: response)
-        }
-    }
-}
-
-private struct WCAnalyticsCustomerEnvelope: Decodable {
-    let customer: [WCAnalyticsCustomer]
-
-    private enum CodingKeys: String, CodingKey {
-        case customer = "data"
-    }
-}
+typealias WCAnalyticsCustomerMapper = SiteIDMapper<[WCAnalyticsCustomer]>
diff --git a/Networking/Networking/Mapper/WCPayAccountMapper.swift b/Networking/Networking/Mapper/WCPayAccountMapper.swift
index 5aed6bb5115..26c2c70e259 100644
--- a/Networking/Networking/Mapper/WCPayAccountMapper.swift
+++ b/Networking/Networking/Mapper/WCPayAccountMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: WCPay account
 ///
 struct WCPayAccountMapper: Mapper {
@@ -16,37 +14,12 @@ struct WCPayAccountMapper: Mapper {
 
         /// Prior to WooCommerce Payments plugin version 2.9.0 (Aug 2021) `data` could contain an empty array []
         /// indicating that the plugin was active but the merchant had not on-boarded (and therefore has no account.)
-        if let _ = try? decoder.decode(WCPayNullAccountEnvelope.self, from: response) {
+        if let _ = try? decoder.decode(Envelope<[String]>.self, from: response) {
             return WCPayAccount.noAccount
         } else if let _ = try? decoder.decode([String].self, from: response) {
             return WCPayAccount.noAccount
         }
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(WCPayAccountEnvelope.self, from: response).account
-        } else {
-            return try decoder.decode(WCPayAccount.self, from: response)
-        }
-    }
-}
-
-private struct WCPayNullAccountEnvelope: Decodable {
-    let emptyArray: [String]
-
-    private enum CodingKeys: String, CodingKey {
-        case emptyArray = "data"
-    }
-}
-
-/// WCPayAccountEnvelope Disposable Entity
-///
-/// Account endpoint returns the requested account in the `data` key. This entity
-/// allows us to parse it with JSONDecoder.
-///
-private struct WCPayAccountEnvelope: Decodable {
-    let account: WCPayAccount
-
-    private enum CodingKeys: String, CodingKey {
-        case account = "data"
+        return try extract(from: response, using: decoder)
     }
 }
diff --git a/Networking/Networking/Mapper/WCPayChargeMapper.swift b/Networking/Networking/Mapper/WCPayChargeMapper.swift
index 9e460f38dec..f8959fcc350 100644
--- a/Networking/Networking/Mapper/WCPayChargeMapper.swift
+++ b/Networking/Networking/Mapper/WCPayChargeMapper.swift
@@ -1,5 +1,3 @@
-import Foundation
-
 /// Mapper: WCPayCharge
 ///
 struct WCPayChargeMapper: Mapper {
@@ -16,23 +14,9 @@ struct WCPayChargeMapper: Mapper {
         /// can cross that bridge when we need those decoded.
         decoder.dateDecodingStrategy = .secondsSince1970
 
-        if hasDataEnvelope(in: response) {
-            return try decoder.decode(WCPayChargeEnvelope.self, from: response).charge
-        } else {
-            return try decoder.decode(WCPayCharge.self, from: response)
-        }
-    }
-}
-
-/// WCPayChargeEnvelope Disposable Entity
-///
-/// Account endpoint returns the requested account in the `data` key. This entity
-/// allows us to parse it with JSONDecoder.
-///
-private struct WCPayChargeEnvelope: Decodable {
-    let charge: WCPayCharge
-
-    private enum CodingKeys: String, CodingKey {
-        case charge = "data"
+        return try extract(
+            from: response,
+            using: decoder
+        )
     }
 }