diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 136ec537..eaeb1de5 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -5,6 +5,7 @@ Brief description of what this PR does, and why it is needed.
 ### Checklist
 
 - [ ] New tests have been added or existing tests have been modified
+- [ ] Changelog updated
 
 ### Notes
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc73fc12..b13e6620 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 ### Added
 
+- Created typeclasses for linking extensions to the items they extend [#85](https://github.com/azavea/stac4s/pull/85)
+
 ### Changed
 
 ### Deprecated
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/ItemCollection.scala b/modules/core/src/main/scala/com/azavea/stac4s/ItemCollection.scala
index f36d7027..4725b365 100644
--- a/modules/core/src/main/scala/com/azavea/stac4s/ItemCollection.scala
+++ b/modules/core/src/main/scala/com/azavea/stac4s/ItemCollection.scala
@@ -1,42 +1,73 @@
 package com.azavea.stac4s
 
 import cats.Eq
+import cats.implicits._
 import io.circe._
 import io.circe.refined._
+import io.circe.syntax._
+import shapeless.LabelledGeneric
+import shapeless.ops.record.Keys
 
 final case class ItemCollection(
     _type: String = "FeatureCollection",
     stacVersion: StacVersion,
     stacExtensions: List[String],
     features: List[StacItem],
-    links: List[StacLink]
+    links: List[StacLink],
+    extensionFields: JsonObject = ().asJsonObject
 )
 
 object ItemCollection {
 
+  private val generic      = LabelledGeneric[ItemCollection]
+  private val keys         = Keys[generic.Repr].apply
+  val itemCollectionFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet
+
   implicit val eqItemCollection: Eq[ItemCollection] = Eq.fromUniversalEquals
 
-  implicit val encItemCollection: Encoder[ItemCollection] = Encoder.forProduct5(
-    "type",
-    "stac_version",
-    "stac_extensions",
-    "features",
-    "links"
-  )(itemCollection =>
+  implicit val encItemCollection: Encoder[ItemCollection] = new Encoder[ItemCollection] {
+
+    def apply(collection: ItemCollection): Json = {
+      val baseEncoder: Encoder[ItemCollection] = Encoder.forProduct5(
+        "type",
+        "stac_version",
+        "stac_extensions",
+        "features",
+        "links"
+      )(itemCollection =>
+        (
+          itemCollection._type,
+          itemCollection.stacVersion,
+          itemCollection.stacExtensions,
+          itemCollection.features,
+          itemCollection.links
+        )
+      )
+
+      baseEncoder(collection).deepMerge(collection.extensionFields.asJson)
+    }
+  }
+
+  implicit val decItemCollection: Decoder[ItemCollection] = { c: HCursor =>
     (
-      itemCollection._type,
-      itemCollection.stacVersion,
-      itemCollection.stacExtensions,
-      itemCollection.features,
-      itemCollection.links
+      c.get[String]("type"),
+      c.get[StacVersion]("stac_version"),
+      c.get[Option[List[String]]]("stac_extensions"),
+      c.get[List[StacItem]]("features"),
+      c.get[Option[List[StacLink]]]("links"),
+      c.value.as[JsonObject]
+    ).mapN(
+      (
+          _type: String,
+          stacVersion: StacVersion,
+          extensions: Option[List[String]],
+          features: List[StacItem],
+          links: Option[List[StacLink]],
+          document: JsonObject
+      ) =>
+        ItemCollection(_type, stacVersion, extensions getOrElse Nil, features, links getOrElse Nil, document.filter({
+          case (k, _) => !itemCollectionFields.contains(k)
+        }))
     )
-  )
-
-  implicit val decItemCollection: Decoder[ItemCollection] = Decoder.forProduct5(
-    "type",
-    "stac_version",
-    "stac_extensions",
-    "features",
-    "links"
-  )(ItemCollection.apply)
+  }
 }
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacCatalog.scala b/modules/core/src/main/scala/com/azavea/stac4s/StacCatalog.scala
index efd037d5..6ef62609 100644
--- a/modules/core/src/main/scala/com/azavea/stac4s/StacCatalog.scala
+++ b/modules/core/src/main/scala/com/azavea/stac4s/StacCatalog.scala
@@ -1,7 +1,11 @@
 package com.azavea.stac4s
 
 import cats.Eq
+import cats.implicits._
 import io.circe._
+import io.circe.syntax._
+import shapeless.LabelledGeneric
+import shapeless.ops.record.Keys
 
 final case class StacCatalog(
     stacVersion: String,
@@ -9,25 +13,67 @@ final case class StacCatalog(
     id: String,
     title: Option[String],
     description: String,
-    links: List[StacLink]
+    links: List[StacLink],
+    extensionFields: JsonObject = ().asJsonObject
 )
 
 object StacCatalog {
 
+  private val generic = LabelledGeneric[StacCatalog]
+  private val keys    = Keys[generic.Repr].apply
+  val catalogFields   = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet
+
   implicit val eqStacCatalog: Eq[StacCatalog] = Eq.fromUniversalEquals
 
-  implicit val encCatalog: Encoder[StacCatalog] =
-    Encoder.forProduct6("stac_version", "stac_extensions", "id", "title", "description", "links")(catalog =>
+  implicit val encCatalog: Encoder[StacCatalog] = new Encoder[StacCatalog] {
+
+    def apply(catalog: StacCatalog): Json = {
+      val baseEncoder: Encoder[StacCatalog] =
+        Encoder.forProduct6("stac_version", "stac_extensions", "id", "title", "description", "links")(catalog =>
+          (
+            catalog.stacVersion,
+            catalog.stacExtensions,
+            catalog.id,
+            catalog.title,
+            catalog.description,
+            catalog.links
+          )
+        )
+
+      baseEncoder(catalog).deepMerge(catalog.extensionFields.asJson)
+    }
+  }
+
+  implicit val decCatalog: Decoder[StacCatalog] = { c: HCursor =>
+    (
+      c.get[String]("stac_version"),
+      c.get[List[String]]("stac_extensions"),
+      c.get[String]("id"),
+      c.get[Option[String]]("title"),
+      c.get[String]("description"),
+      c.get[List[StacLink]]("links"),
+      c.value.as[JsonObject]
+    ).mapN(
       (
-        catalog.stacVersion,
-        catalog.stacExtensions,
-        catalog.id,
-        catalog.title,
-        catalog.description,
-        catalog.links
-      )
+          version: String,
+          extensions: List[String],
+          id: String,
+          title: Option[String],
+          description: String,
+          links: List[StacLink],
+          document: JsonObject
+      ) =>
+        StacCatalog.apply(
+          version,
+          extensions,
+          id,
+          title,
+          description,
+          links,
+          document.filter({
+            case (k, _) => !catalogFields.contains(k)
+          })
+        )
     )
-
-  implicit val decCatalog: Decoder[StacCatalog] =
-    Decoder.forProduct6("stac_version", "stac_extensions", "id", "title", "description", "links")(StacCatalog.apply)
+  }
 }
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacCollection.scala b/modules/core/src/main/scala/com/azavea/stac4s/StacCollection.scala
index 86286bde..1461dd34 100644
--- a/modules/core/src/main/scala/com/azavea/stac4s/StacCollection.scala
+++ b/modules/core/src/main/scala/com/azavea/stac4s/StacCollection.scala
@@ -1,8 +1,12 @@
 package com.azavea.stac4s
 
 import cats.Eq
+import cats.implicits._
 import geotrellis.vector.{io => _}
 import io.circe._
+import io.circe.syntax._
+import shapeless.LabelledGeneric
+import shapeless.ops.record.Keys
 
 final case class StacCollection(
     stacVersion: String,
@@ -14,60 +18,76 @@ final case class StacCollection(
     license: StacLicense,
     providers: List[StacProvider],
     extent: StacExtent,
+    summaries: JsonObject,
     properties: JsonObject,
-    links: List[StacLink]
+    links: List[StacLink],
+    extensionFields: JsonObject = ().asJsonObject
 )
 
 object StacCollection {
 
+  private val generic  = LabelledGeneric[StacCollection]
+  private val keys     = Keys[generic.Repr].apply
+  val collectionFields = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet
+
   implicit val eqStacCollection: Eq[StacCollection] = Eq.fromUniversalEquals
 
-  implicit val encoderStacCollection: Encoder[StacCollection] =
-    Encoder.forProduct11(
-      "stac_version",
-      "stac_extensions",
-      "id",
-      "title",
-      "description",
-      "keywords",
-      "license",
-      "providers",
-      "extent",
-      "properties",
-      "links"
-    )(collection =>
-      (
-        collection.stacVersion,
-        collection.stacExtensions,
-        collection.id,
-        collection.title,
-        collection.description,
-        collection.keywords,
-        collection.license,
-        collection.providers,
-        collection.extent,
-        collection.properties,
-        collection.links
+  implicit val encoderStacCollection: Encoder[StacCollection] = new Encoder[StacCollection] {
+
+    def apply(collection: StacCollection): Json = {
+      val baseEncoder: Encoder[StacCollection] = Encoder.forProduct12(
+        "stac_version",
+        "stac_extensions",
+        "id",
+        "title",
+        "description",
+        "keywords",
+        "license",
+        "providers",
+        "extent",
+        "summaries",
+        "properties",
+        "links"
+      )(collection =>
+        (
+          collection.stacVersion,
+          collection.stacExtensions,
+          collection.id,
+          collection.title,
+          collection.description,
+          collection.keywords,
+          collection.license,
+          collection.providers,
+          collection.extent,
+          collection.summaries,
+          collection.properties,
+          collection.links
+        )
       )
-    )
 
-  implicit val decoderStacCollection: Decoder[StacCollection] =
-    Decoder.forProduct11(
-      "stac_version",
-      "stac_extensions",
-      "id",
-      "title",
-      "description",
-      "keywords",
-      "license",
-      "providers",
-      "extent",
-      "properties",
-      "links"
-    )(
+      baseEncoder(collection).deepMerge(collection.extensionFields.asJson)
+    }
+  }
+
+  implicit val decoderStacCollection: Decoder[StacCollection] = { c: HCursor =>
+    (
+      c.get[String]("stac_version"),
+      c.get[Option[List[String]]]("stac_extensions"),
+      c.get[String]("id"),
+      c.get[Option[String]]("title"),
+      c.get[String]("description"),
+      c.get[Option[List[String]]]("keywords"),
+      c.get[StacLicense]("license"),
+      c.get[Option[List[StacProvider]]]("providers"),
+      c.get[StacExtent]("extent"),
+      c.get[Option[JsonObject]]("summaries"),
+      c.get[JsonObject]("properties"),
+      c.get[List[StacLink]]("links"),
+      c.value.as[JsonObject]
+    ).mapN(
       (
           stacVersion: String,
-          stacExtensions: List[String],
+          stacExtensions: Option[List[String]],
           id: String,
           title: Option[String],
           description: String,
@@ -75,12 +95,14 @@ object StacCollection {
           license: StacLicense,
           providers: Option[List[StacProvider]],
           extent: StacExtent,
-          properties: Option[JsonObject],
-          links: List[StacLink]
+          summaries: Option[JsonObject],
+          properties: JsonObject,
+          links: List[StacLink],
+          extensionFields: JsonObject
       ) =>
         StacCollection(
           stacVersion,
-          stacExtensions,
+          stacExtensions getOrElse Nil,
           id,
           title,
           description,
@@ -88,8 +110,13 @@ object StacCollection {
           license,
           providers getOrElse List.empty,
           extent,
-          properties getOrElse JsonObject.fromMap(Map.empty),
-          links
+          summaries getOrElse JsonObject.fromMap(Map.empty),
+          properties,
+          links,
+          extensionFields.filter({
+            case (k, _) => !collectionFields.contains(k)
+          })
         )
     )
+  }
 }
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala b/modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala
index 3bd7141c..400322be 100644
--- a/modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala
+++ b/modules/core/src/main/scala/com/azavea/stac4s/StacItemAsset.scala
@@ -1,27 +1,61 @@
 package com.azavea.stac4s
 
 import cats.Eq
+import cats.implicits._
 import io.circe._
+import io.circe.syntax._
+import shapeless.LabelledGeneric
+import shapeless.ops.record.Keys
 
 final case class StacItemAsset(
     href: String,
     title: Option[String],
     description: Option[String],
-    roles: List[StacAssetRole],
-    _type: Option[StacMediaType]
+    roles: Set[StacAssetRole],
+    _type: Option[StacMediaType],
+    extensionFields: JsonObject = ().asJsonObject
 )
 
 object StacItemAsset {
 
+  private val generic = LabelledGeneric[StacItemAsset]
+  private val keys    = Keys[generic.Repr].apply
+  val assetFields     = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet
+
   implicit val eqStacItemAsset: Eq[StacItemAsset] = Eq.fromUniversalEquals
 
-  implicit val encStacItemAsset: Encoder[StacItemAsset] =
-    Encoder.forProduct5("href", "title", "description", "roles", "type")(asset =>
-      (asset.href, asset.title, asset.description, asset.roles, asset._type)
-    )
+  implicit val encStacItemAsset: Encoder[StacItemAsset] = new Encoder[StacItemAsset] {
+
+    def apply(asset: StacItemAsset): Json = {
+
+      val baseEncoder: Encoder[StacItemAsset] =
+        Encoder.forProduct5("href", "title", "description", "roles", "type")(asset =>
+          (asset.href, asset.title, asset.description, asset.roles, asset._type)
+        )
+      baseEncoder(asset).deepMerge(asset.extensionFields.asJson)
+    }
+  }
 
-  implicit val decStacItemAsset: Decoder[StacItemAsset] =
-    Decoder.forProduct5("href", "title", "description", "roles", "type")(
-      StacItemAsset.apply
+  implicit val decStacItemAsset: Decoder[StacItemAsset] = { c: HCursor =>
+    (
+      c.get[String]("href"),
+      c.get[Option[String]]("title"),
+      c.get[Option[String]]("description"),
+      c.get[Option[Set[StacAssetRole]]]("roles"),
+      c.get[Option[StacMediaType]]("type"),
+      c.value.as[JsonObject]
+    ).mapN(
+      (
+          href: String,
+          title: Option[String],
+          description: Option[String],
+          roles: Option[Set[StacAssetRole]],
+          mediaType: Option[StacMediaType],
+          document: JsonObject
+      ) =>
+        StacItemAsset(href, title, description, roles getOrElse Set.empty, mediaType, document.filter({
+          case (k, _) => !assetFields.contains(k)
+        }))
     )
+  }
 }
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacLink.scala b/modules/core/src/main/scala/com/azavea/stac4s/StacLink.scala
index 156bff9d..81bdee97 100644
--- a/modules/core/src/main/scala/com/azavea/stac4s/StacLink.scala
+++ b/modules/core/src/main/scala/com/azavea/stac4s/StacLink.scala
@@ -2,24 +2,38 @@ package com.azavea.stac4s
 
 import cats.implicits._
 import io.circe._
+import io.circe.syntax._
+import shapeless.LabelledGeneric
+import shapeless.ops.record.Keys
 
 final case class StacLink(
     href: String,
     rel: StacLinkType,
     _type: Option[StacMediaType],
     title: Option[String],
-    labelExtAssets: List[String]
+    extensionFields: JsonObject = ().asJsonObject
 )
 
 object StacLink {
 
-  implicit val encStacLink: Encoder[StacLink] = Encoder.forProduct5(
-    "href",
-    "rel",
-    "type",
-    "title",
-    "label:assets"
-  )(link => (link.href, link.rel, link._type, link.title, link.labelExtAssets))
+  private val generic = LabelledGeneric[StacLink]
+  private val keys    = Keys[generic.Repr].apply
+  val linkFields      = keys.toList.flatMap(field => substituteFieldName(field.name)).toSet
+
+  implicit val encStacLink: Encoder[StacLink] = new Encoder[StacLink] {
+
+    def apply(link: StacLink): Json = {
+      val baseEncoder = Encoder
+        .forProduct4(
+          "href",
+          "rel",
+          "type",
+          "title"
+        )((link: StacLink) => (link.href, link.rel, link._type, link.title))
+
+      baseEncoder(link).deepMerge(link.extensionFields.asJson)
+    }
+  }
 
   implicit val decStacLink: Decoder[StacLink] = { c: HCursor =>
     (
@@ -27,15 +41,18 @@ object StacLink {
       c.downField("rel").as[StacLinkType],
       c.get[Option[StacMediaType]]("type"),
       c.get[Option[String]]("title"),
-      c.get[Option[List[String]]]("label:assets")
+      c.value.as[JsonObject]
     ).mapN(
       (
           href: String,
           rel: StacLinkType,
           _type: Option[StacMediaType],
           title: Option[String],
-          assets: Option[List[String]]
-      ) => StacLink(href, rel, _type, title, assets getOrElse List.empty)
+          document: JsonObject
+      ) =>
+        StacLink(href, rel, _type, title, document.filter({
+          case (k, _) => !linkFields.contains(k)
+        }))
     )
   }
 }
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/CatalogExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/CatalogExtension.scala
new file mode 100644
index 00000000..88047971
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/CatalogExtension.scala
@@ -0,0 +1,25 @@
+package com.azavea.stac4s.extensions
+
+import com.azavea.stac4s.StacCatalog
+
+import io.circe.{Decoder, Encoder}
+import io.circe.syntax._
+
+trait CatalogExtension[T] {
+  def getExtensionFields(catalog: StacCatalog): ExtensionResult[T]
+  def addExtensionFields(catalog: StacCatalog, extensionFields: T): StacCatalog
+}
+
+object CatalogExtension {
+  def apply[T](implicit ev: CatalogExtension[T]): CatalogExtension[T] = ev
+
+  def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]) =
+    new CatalogExtension[T] {
+
+      def getExtensionFields(catalog: StacCatalog): ExtensionResult[T] =
+        decoder.decodeAccumulating(catalog.extensionFields.asJson.hcursor)
+
+      def addExtensionFields(catalog: StacCatalog, extensionFields: T): StacCatalog =
+        catalog.copy(extensionFields = catalog.extensionFields.deepMerge(objectEncoder.encodeObject(extensionFields)))
+    }
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/CollectionExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/CollectionExtension.scala
new file mode 100644
index 00000000..4306b832
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/CollectionExtension.scala
@@ -0,0 +1,28 @@
+package com.azavea.stac4s.extensions
+
+import com.azavea.stac4s.StacCollection
+
+import io.circe.{Decoder, Encoder}
+import io.circe.syntax._
+
+trait CollectionExtension[T] {
+  def getExtensionFields(collection: StacCollection): ExtensionResult[T]
+
+  def addExtensionFields(collection: StacCollection, extensionFields: T): StacCollection
+}
+
+object CollectionExtension {
+  def apply[T](implicit ev: CollectionExtension[T]): CollectionExtension[T] = ev
+
+  def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]) =
+    new CollectionExtension[T] {
+
+      def getExtensionFields(collection: StacCollection): ExtensionResult[T] =
+        decoder.decodeAccumulating(collection.extensionFields.asJson.hcursor)
+
+      def addExtensionFields(collection: StacCollection, extensionFields: T): StacCollection =
+        collection.copy(extensionFields =
+          collection.extensionFields.deepMerge(objectEncoder.encodeObject(extensionFields))
+        )
+    }
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemAssetExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemAssetExtension.scala
new file mode 100644
index 00000000..3b910f39
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemAssetExtension.scala
@@ -0,0 +1,25 @@
+package com.azavea.stac4s.extensions
+
+import com.azavea.stac4s.StacItemAsset
+
+import io.circe.{Decoder, Encoder}
+import io.circe.syntax._
+
+trait ItemAssetExtension[T] {
+  def getExtensionFields(asset: StacItemAsset): ExtensionResult[T]
+  def addExtensionFields(asset: StacItemAsset, extensionFields: T): StacItemAsset
+}
+
+object ItemAssetExtension {
+  def apply[T](implicit ev: ItemAssetExtension[T]): ItemAssetExtension[T] = ev
+
+  def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]) =
+    new ItemAssetExtension[T] {
+
+      def getExtensionFields(asset: StacItemAsset): ExtensionResult[T] =
+        decoder.decodeAccumulating(asset.extensionFields.asJson.hcursor)
+
+      def addExtensionFields(asset: StacItemAsset, extensionFields: T): StacItemAsset =
+        asset.copy(extensionFields = asset.extensionFields.deepMerge(objectEncoder.encodeObject(extensionFields)))
+    }
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemCollectionExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemCollectionExtension.scala
new file mode 100644
index 00000000..12ee2cca
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemCollectionExtension.scala
@@ -0,0 +1,29 @@
+package com.azavea.stac4s.extensions
+
+import com.azavea.stac4s._
+
+import io.circe.{Decoder, Encoder}
+import io.circe.syntax._
+
+trait ItemCollectionExtension[T] {
+  def getExtensionFields(itemCollection: ItemCollection): ExtensionResult[T]
+  def addExtensionFields(itemCollection: ItemCollection, properties: T): ItemCollection
+}
+
+object ItemExtensionCollection {
+  def apply[T](implicit ev: ItemCollectionExtension[T]): ItemCollectionExtension[T] = ev
+
+  def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]): ItemCollectionExtension[T] =
+    new ItemCollectionExtension[T] {
+
+      def getExtensionFields(itemCollection: ItemCollection): ExtensionResult[T] =
+        decoder.decodeAccumulating(
+          itemCollection.extensionFields.asJson.hcursor
+        )
+
+      def addExtensionFields(itemCollection: ItemCollection, extensionProperties: T): ItemCollection =
+        itemCollection.copy(extensionFields =
+          itemCollection.extensionFields.deepMerge(objectEncoder.encodeObject(extensionProperties))
+        )
+    }
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemExtension.scala
new file mode 100644
index 00000000..968ec3d2
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/ItemExtension.scala
@@ -0,0 +1,30 @@
+package com.azavea.stac4s.extensions
+
+import com.azavea.stac4s.StacItem
+
+import io.circe._
+import io.circe.syntax._
+
+// typeclass trait for anything that is an extension of item properties
+trait ItemExtension[T] {
+  def getExtensionFields(item: StacItem): ExtensionResult[T]
+  def addExtensionFields(item: StacItem, properties: T): StacItem
+}
+
+object ItemExtension {
+  // summoner
+  def apply[T](implicit ev: ItemExtension[T]): ItemExtension[T] = ev
+
+  // constructor for anything with a `Decoder` and an `Encoder.AsObject`
+  def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]): ItemExtension[T] =
+    new ItemExtension[T] {
+
+      def getExtensionFields(item: StacItem): ExtensionResult[T] =
+        decoder.decodeAccumulating(
+          item.properties.asJson.hcursor
+        )
+
+      def addExtensionFields(item: StacItem, extensionProperties: T) =
+        item.copy(properties = item.properties.deepMerge(objectEncoder.encodeObject(extensionProperties)))
+    }
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/LinkExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/LinkExtension.scala
new file mode 100644
index 00000000..fdfc9e59
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/LinkExtension.scala
@@ -0,0 +1,26 @@
+package com.azavea.stac4s.extensions
+
+import com.azavea.stac4s.StacLink
+
+import io.circe.{Decoder, Encoder}
+import io.circe.syntax._
+
+trait LinkExtension[T] {
+  def getExtensionFields(link: StacLink): ExtensionResult[T]
+
+  def addExtensionFields(link: StacLink, extensionFields: T): StacLink
+}
+
+object LinkExtension {
+  def apply[T](implicit ev: LinkExtension[T]): LinkExtension[T] = ev
+
+  def instance[T](implicit decoder: Decoder[T], objectEncoder: Encoder.AsObject[T]) =
+    new LinkExtension[T] {
+
+      def getExtensionFields(link: StacLink): ExtensionResult[T] =
+        decoder.decodeAccumulating(link.extensionFields.asJson.hcursor)
+
+      def addExtensionFields(link: StacLink, extensionFields: T): StacLink =
+        link.copy(extensionFields = link.extensionFields.deepMerge(objectEncoder.encodeObject(extensionFields)))
+    }
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/AssetCollectionExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/AssetCollectionExtension.scala
new file mode 100644
index 00000000..458570e1
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/AssetCollectionExtension.scala
@@ -0,0 +1,23 @@
+package com.azavea.stac4s.extensions.asset
+
+import com.azavea.stac4s.extensions.CollectionExtension
+
+import io.circe._
+import io.circe.syntax._
+
+final case class AssetCollectionExtension(
+    assets: Map[String, StacCollectionAsset]
+)
+
+object AssetCollectionExtension {
+
+  implicit val encAssetCollectionExtension: Encoder.AsObject[AssetCollectionExtension] = Encoder
+    .AsObject[Map[String, Json]]
+    .contramapObject((extensionFields: AssetCollectionExtension) => Map("assets" -> extensionFields.assets.asJson))
+
+  implicit val decAssetCollectionExtension: Decoder[AssetCollectionExtension] =
+    Decoder.forProduct1("assets")(AssetCollectionExtension.apply)
+
+  implicit val collectionExtension: CollectionExtension[AssetCollectionExtension] =
+    CollectionExtension.instance
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/StacCollectionAsset.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/StacCollectionAsset.scala
similarity index 90%
rename from modules/core/src/main/scala/com/azavea/stac4s/StacCollectionAsset.scala
rename to modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/StacCollectionAsset.scala
index 0dfe4256..71a26712 100644
--- a/modules/core/src/main/scala/com/azavea/stac4s/StacCollectionAsset.scala
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/asset/StacCollectionAsset.scala
@@ -1,4 +1,6 @@
-package com.azavea.stac4s
+package com.azavea.stac4s.extensions.asset
+
+import com.azavea.stac4s._
 
 import cats.Eq
 import io.circe._
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelExtensionProperties.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelItemExtension.scala
similarity index 53%
rename from modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelExtensionProperties.scala
rename to modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelItemExtension.scala
index 315df1c5..2907b009 100644
--- a/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelExtensionProperties.scala
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelItemExtension.scala
@@ -1,10 +1,13 @@
 package com.azavea.stac4s.extensions.label
 
+import com.azavea.stac4s.extensions.ItemExtension
+
 import cats.Eq
 import cats.implicits._
-import io.circe.{Decoder, Encoder, HCursor}
+import io.circe.{Decoder, Encoder, HCursor, Json}
+import io.circe.syntax._
 
-case class LabelExtensionProperties(
+case class LabelItemExtension(
     properties: LabelProperties,
     classes: List[LabelClass],
     description: String,
@@ -14,29 +17,23 @@ case class LabelExtensionProperties(
     overviews: List[LabelOverview]
 )
 
-object LabelExtensionProperties {
+object LabelItemExtension {
 
-  implicit val encLabelExtensionProperties: Encoder[LabelExtensionProperties] = Encoder.forProduct7(
-    "label:properties",
-    "label:classes",
-    "label:description",
-    "label:type",
-    "label:tasks",
-    "label:methods",
-    "label:overviews"
-  )(extensionProps =>
-    (
-      extensionProps.properties,
-      extensionProps.classes,
-      extensionProps.description,
-      extensionProps._type,
-      extensionProps.tasks,
-      extensionProps.methods,
-      extensionProps.overviews
+  implicit val encLabelExtensionPropertiesObject: Encoder.AsObject[LabelItemExtension] = Encoder
+    .AsObject[Map[String, Json]]
+    .contramapObject((properties: LabelItemExtension) =>
+      Map(
+        "label:properties"  -> properties.properties.asJson,
+        "label:classes"     -> properties.classes.asJson,
+        "label:description" -> properties.description.asJson,
+        "label:type"        -> properties._type.asJson,
+        "label:tasks"       -> properties.tasks.asJson,
+        "label:methods"     -> properties.methods.asJson,
+        "label:overviews"   -> properties.overviews.asJson
+      )
     )
-  )
 
-  implicit val decLabelExtensionProperties: Decoder[LabelExtensionProperties] = new Decoder[LabelExtensionProperties] {
+  implicit val decLabelExtensionProperties: Decoder[LabelItemExtension] = new Decoder[LabelItemExtension] {
 
     def apply(c: HCursor) =
       (
@@ -57,7 +54,7 @@ object LabelExtensionProperties {
             methods: Option[List[LabelMethod]],
             overviews: Option[List[LabelOverview]]
         ) =>
-          LabelExtensionProperties(
+          LabelItemExtension(
             properties,
             classes,
             description,
@@ -69,5 +66,7 @@ object LabelExtensionProperties {
       )
   }
 
-  implicit val eqLabelExtensionProperties: Eq[LabelExtensionProperties] = Eq.fromUniversalEquals
+  implicit val eqLabelExtensionProperties: Eq[LabelItemExtension] = Eq.fromUniversalEquals
+
+  implicit val itemExtensionLabelProperties: ItemExtension[LabelItemExtension] = ItemExtension.instance
 }
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelLinkExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelLinkExtension.scala
new file mode 100644
index 00000000..c94531c5
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/label/LabelLinkExtension.scala
@@ -0,0 +1,21 @@
+package com.azavea.stac4s.extensions.label
+
+import com.azavea.stac4s.extensions.LinkExtension
+
+import cats.data.NonEmptyList
+import io.circe.{Decoder, Encoder, Json}
+import io.circe.syntax._
+
+case class LabelLinkExtension(assets: NonEmptyList[String])
+
+object LabelLinkExtension {
+
+  implicit val encLabelLinkExtensionObject: Encoder.AsObject[LabelLinkExtension] = Encoder
+    .AsObject[Map[String, Json]]
+    .contramapObject((extensionFields: LabelLinkExtension) => Map("label:assets" -> extensionFields.assets.asJson))
+
+  implicit val decLabelLinkExtension: Decoder[LabelLinkExtension] =
+    Decoder.forProduct1("label:assets")(LabelLinkExtension.apply)
+
+  implicit val linkExtension: LinkExtension[LabelLinkExtension] = LinkExtension.instance
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerItemExtension.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerItemExtension.scala
new file mode 100644
index 00000000..53b3891c
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerItemExtension.scala
@@ -0,0 +1,24 @@
+package com.azavea.stac4s.extensions.layer
+
+import com.azavea.stac4s.extensions.ItemExtension
+
+import cats.data.NonEmptyList
+import cats.Eq
+import eu.timepit.refined.types.string.NonEmptyString
+import io.circe.{Decoder, Encoder}
+import io.circe.refined._
+import io.circe.syntax._
+
+case class LayerItemExtension(ids: NonEmptyList[NonEmptyString])
+
+object LayerItemExtension {
+  implicit val eqLayerProperties: Eq[LayerItemExtension] = Eq.fromUniversalEquals
+
+  implicit val encLayerProperties: Encoder.AsObject[LayerItemExtension] =
+    Encoder.AsObject.instance[LayerItemExtension] { o => Map("layer:ids" -> o.ids.asJson).asJsonObject }
+
+  implicit val decLayerProperties: Decoder[LayerItemExtension] =
+    Decoder.forProduct1("layer:ids")(LayerItemExtension.apply)
+
+  implicit val itemExtension: ItemExtension[LayerItemExtension] = ItemExtension.instance
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerProperties.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerProperties.scala
deleted file mode 100644
index ac87b122..00000000
--- a/modules/core/src/main/scala/com/azavea/stac4s/extensions/layer/LayerProperties.scala
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.azavea.stac4s.extensions.layer
-
-import cats.Eq
-import io.circe.{Decoder, Encoder}
-import io.circe.syntax._
-
-case class LayerProperties(ids: List[String])
-
-object LayerProperties {
-  implicit val eqLayerProperties: Eq[LayerProperties] = Eq.fromUniversalEquals
-
-  implicit val encLayerProperties: Encoder.AsObject[LayerProperties] = Encoder.AsObject.instance[LayerProperties] { o =>
-    Map("layer:ids" -> o.ids.asJson).asJsonObject
-  }
-
-  implicit val decLayerProperties: Decoder[LayerProperties] = Decoder[Map[String, List[String]]] emap {
-    _.get("layer:ids") match {
-      case Some(l) => Right(LayerProperties(l))
-      case _       => Left("Could not decode LayerProperties.")
-    }
-  }
-}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/extensions/package.scala b/modules/core/src/main/scala/com/azavea/stac4s/extensions/package.scala
new file mode 100644
index 00000000..18ef87b8
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/extensions/package.scala
@@ -0,0 +1,13 @@
+package com.azavea.stac4s
+
+import cats.data.ValidatedNel
+import io.circe.Error
+
+package object extensions {
+
+  // convenience type not to have to write ValidatedNel in a few places /
+  // to expose a nicer API to users (a la MAML:
+  // https://github.com/geotrellis/maml/blob/713c6a0c54646d1972855bf5a1f0efddd108f95d/shared/src/main/scala/error/package.scala#L8)
+  type ExtensionResult[T] = ValidatedNel[Error, T]
+
+}
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/package.scala b/modules/core/src/main/scala/com/azavea/stac4s/package.scala
index 71026161..d5548a5b 100644
--- a/modules/core/src/main/scala/com/azavea/stac4s/package.scala
+++ b/modules/core/src/main/scala/com/azavea/stac4s/package.scala
@@ -39,4 +39,11 @@ package object stac4s {
 
   implicit val eqTemporalExtent: Eq[TemporalExtent] = Eq.fromUniversalEquals
 
+  def substituteFieldName(fieldName: String): Option[String] = fieldName match {
+    case "_type"           => Some("type")
+    case "stacVersion"     => Some("stac_version")
+    case "stacExtensions"  => Some("stac_extensions")
+    case "extensionFields" => None
+    case s                 => Some(s)
+  }
 }
diff --git a/modules/core/src/main/scala/com/azavea/stac4s/syntax/package.scala b/modules/core/src/main/scala/com/azavea/stac4s/syntax/package.scala
new file mode 100644
index 00000000..f2b093b4
--- /dev/null
+++ b/modules/core/src/main/scala/com/azavea/stac4s/syntax/package.scala
@@ -0,0 +1,60 @@
+package com.azavea.stac4s
+
+import com.azavea.stac4s.extensions._
+
+package object syntax {
+
+  implicit class stacItemExtensions(item: StacItem) {
+
+    def getExtensionFields[T](implicit ev: ItemExtension[T]): ExtensionResult[T] =
+      ev.getExtensionFields(item)
+
+    def addExtensionFields[T](properties: T)(implicit ev: ItemExtension[T]): StacItem =
+      ev.addExtensionFields(item, properties)
+  }
+
+  implicit class stacCollectionExtensions(collection: StacCollection) {
+
+    def getExtensionFields[T](implicit ev: CollectionExtension[T]): ExtensionResult[T] =
+      ev.getExtensionFields(collection)
+
+    def addExtensionFields[T](properties: T)(implicit ev: CollectionExtension[T]): StacCollection =
+      ev.addExtensionFields(collection, properties)
+  }
+
+  implicit class stacCatalogExtensions(catalog: StacCatalog) {
+
+    def getExtensionFields[T](implicit ev: CatalogExtension[T]): ExtensionResult[T] =
+      ev.getExtensionFields(catalog)
+
+    def addExtensionFields[T](properties: T)(implicit ev: CatalogExtension[T]): StacCatalog =
+      ev.addExtensionFields(catalog, properties)
+  }
+
+  implicit class stacItemAssetExtensions(itemAsset: StacItemAsset) {
+
+    def getExtensionFields[T](implicit ev: ItemAssetExtension[T]): ExtensionResult[T] =
+      ev.getExtensionFields(itemAsset)
+
+    def addExtensionFields[T](properties: T)(implicit ev: ItemAssetExtension[T]): StacItemAsset =
+      ev.addExtensionFields(itemAsset, properties)
+  }
+
+  implicit class stacLinkExtensions(link: StacLink) {
+
+    def getExtensionFields[T](implicit ev: LinkExtension[T]): ExtensionResult[T] =
+      ev.getExtensionFields(link)
+
+    def addExtensionFields[T](properties: T)(implicit ev: LinkExtension[T]): StacLink =
+      ev.addExtensionFields(link, properties)
+  }
+
+  implicit class stacItemCollectionExtensions(itemCollection: ItemCollection) {
+
+    def getExtensionFields[T](implicit ev: ItemCollectionExtension[T]): ExtensionResult[T] =
+      ev.getExtensionFields(itemCollection)
+
+    def addExtensionFields[T](properties: T)(implicit ev: ItemCollectionExtension[T]): ItemCollection =
+      ev.addExtensionFields(itemCollection, properties)
+  }
+}
diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/catalog.json
index 93db0fed..d4cc41b2 100644
--- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/catalog.json
+++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/catalog.json
@@ -7,28 +7,23 @@
   "links" : [
     {
       "href" : "./catalog.json",
-      "rel" : "self",
-      "label:assets" : []
+      "rel" : "self"
     },
     {
       "href" : "./catalog.json",
-      "rel" : "root",
-      "label:assets" : []
+      "rel" : "root"
     },
     {
       "href" : "./landsat-8-l1/catalog.json",
-      "rel" : "child",
-      "label:assets" : []
+      "rel" : "child"
     },
     {
       "href" : "./layers/ca/catalog.json",
-      "rel" : "child",
-      "label:assets" : []
+      "rel" : "child"
     },
     {
       "href" : "./layers/us/catalog.json",
-      "rel" : "child",
-      "label:assets" : []
+      "rel" : "child"
     }
   ]
 }
diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json
index 5261bbdd..7162167e 100644
--- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json
+++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json
@@ -44,28 +44,20 @@
   "links" : [
     {
       "href" : "../../catalog.json",
-      "rel" : "root",
-      "label:assets" : [
-      ]
+      "rel" : "root"
     },
     {
       "href" : "../catalog.json",
-      "rel" : "parent",
-      "label:assets" : [
-      ]
+      "rel" : "parent"
     },
     {
       "href" : "./LC81530252014153LGN00.json",
-      "rel" : "self",
-      "label:assets" : [
-      ]
+      "rel" : "self"
     },
     {
       "href" : "https://landsatonaws.com/L8/153/025/LC81530252014153LGN0",
       "rel" : "alternate",
-      "type" : "text/html",
-      "label:assets" : [
-      ]
+      "type" : "text/html"
     }
   ],
   "assets" : {
diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/catalog.json
index 5f7c3c3f..7fb70822 100644
--- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/catalog.json
+++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/landsat-8-l1/catalog.json
@@ -135,27 +135,19 @@
   "links" : [
     {
       "href" : "../../catalog.json",
-      "rel" : "root",
-      "label:assets" : [
-      ]
+      "rel" : "root"
     },
     {
       "href" : "../../catalog.json",
-      "rel" : "parent",
-      "label:assets" : [
-      ]
+      "rel" : "parent"
     },
     {
       "href" : "./catalog.json",
-      "rel" : "self",
-      "label:assets" : [
-      ]
+      "rel" : "self"
     },
     {
       "href" : "./2014-153/LC81530252014153LGN00.json",
-      "rel" : "item",
-      "label:assets" : [
-      ]
+      "rel" : "item"
     }
   ]
 }
diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/ca/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/ca/catalog.json
index 5119a764..4831425d 100644
--- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/ca/catalog.json
+++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/ca/catalog.json
@@ -11,27 +11,19 @@
   "links" : [
     {
       "href" : "../catalog.json",
-      "rel" : "root",
-      "label:assets" : [
-      ]
+      "rel" : "root"
     },
     {
       "href" : "../catalog.json",
-      "rel" : "parent",
-      "label:assets" : [
-      ]
+      "rel" : "parent"
     },
     {
       "href" : "./catalog.json",
-      "rel" : "self",
-      "label:assets" : [
-      ]
+      "rel" : "self"
     },
     {
       "href" : "../../landsat-8-l1/2014-153/LC81530252014153LGN00.json",
-      "rel" : "item",
-      "label:assets" : [
-      ]
+      "rel" : "item"
     }
   ]
 }
diff --git a/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/us/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/us/catalog.json
index 443e83d7..fb2e08cd 100644
--- a/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/us/catalog.json
+++ b/modules/core/src/test/resources/catalogs/landsat-stac-layers/layers/us/catalog.json
@@ -11,27 +11,19 @@
   "links" : [
     {
       "href" : "../catalog.json",
-      "rel" : "root",
-      "label:assets" : [
-      ]
+      "rel" : "root"
     },
     {
       "href" : "../catalog.json",
-      "rel" : "parent",
-      "label:assets" : [
-      ]
+      "rel" : "parent"
     },
     {
       "href" : "./catalog.json",
-      "rel" : "self",
-      "label:assets" : [
-      ]
+      "rel" : "self"
     },
     {
       "href" : "../../landsat-8-l1/2014-153/LC81530252014153LGN00.json",
-      "rel" : "item",
-      "label:assets" : [
-      ]
+      "rel" : "item"
     }
   ]
 }
diff --git a/modules/core/src/test/resources/catalogs/landsat-stac/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac/catalog.json
index 23bf7e91..8ea41e9b 100644
--- a/modules/core/src/test/resources/catalogs/landsat-stac/catalog.json
+++ b/modules/core/src/test/resources/catalogs/landsat-stac/catalog.json
@@ -7,18 +7,15 @@
   "links" : [
     {
       "href" : "./catalog.json",
-      "rel" : "self",
-      "label:assets" : []
+      "rel" : "self"
     },
     {
       "href" : "./catalog.json",
-      "rel" : "root",
-      "label:assets" : []
+      "rel" : "root"
     },
     {
       "href" : "./landsat-8-l1/catalog.json",
-      "rel" : "child",
-      "label:assets" : []
+      "rel" : "child"
     }
   ]
 }
diff --git a/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/2014-153/LC81530252014153LGN00.json b/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/2014-153/LC81530252014153LGN00.json
index 5fee655a..0135797a 100644
--- a/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/2014-153/LC81530252014153LGN00.json
+++ b/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/2014-153/LC81530252014153LGN00.json
@@ -43,28 +43,20 @@
   "links" : [
     {
       "href" : "../../catalog.json",
-      "rel" : "root",
-      "label:assets" : [
-      ]
+      "rel" : "root"
     },
     {
       "href" : "../catalog.json",
-      "rel" : "parent",
-      "label:assets" : [
-      ]
+      "rel" : "parent"
     },
     {
       "href" : "./LC81530252014153LGN00.json",
-      "rel" : "self",
-      "label:assets" : [
-      ]
+      "rel" : "self"
     },
     {
       "href" : "https://landsatonaws.com/L8/153/025/LC81530252014153LGN0",
       "rel" : "alternate",
-      "type" : "text/html",
-      "label:assets" : [
-      ]
+      "type" : "text/html"
     }
   ],
   "assets" : {
diff --git a/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json b/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json
index 5f7c3c3f..2d80fbaa 100644
--- a/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json
+++ b/modules/core/src/test/resources/catalogs/landsat-stac/landsat-8-l1/catalog.json
@@ -43,6 +43,7 @@
       ]
     }
   },
+  "summaries": {},
   "properties" : {
     "collection" : "landsat-8-l1",
     "eo:instrument" : "OLI_TIRS",
@@ -135,27 +136,19 @@
   "links" : [
     {
       "href" : "../../catalog.json",
-      "rel" : "root",
-      "label:assets" : [
-      ]
+      "rel" : "root"
     },
     {
       "href" : "../../catalog.json",
-      "rel" : "parent",
-      "label:assets" : [
-      ]
+      "rel" : "parent"
     },
     {
       "href" : "./catalog.json",
-      "rel" : "self",
-      "label:assets" : [
-      ]
+      "rel" : "self"
     },
     {
       "href" : "./2014-153/LC81530252014153LGN00.json",
-      "rel" : "item",
-      "label:assets" : [
-      ]
+      "rel" : "item"
     }
   ]
 }
diff --git a/modules/core/src/test/scala/com/azavea/stac4s/CatalogLayerSpec.scala b/modules/core/src/test/scala/com/azavea/stac4s/CatalogLayerSpec.scala
index fb873bef..61799ac7 100644
--- a/modules/core/src/test/scala/com/azavea/stac4s/CatalogLayerSpec.scala
+++ b/modules/core/src/test/scala/com/azavea/stac4s/CatalogLayerSpec.scala
@@ -1,10 +1,12 @@
 package com.azavea.stac4s
 
+import com.azavea.stac4s.extensions.ItemExtension
 import com.azavea.stac4s.extensions.layer._
+import cats.data.NonEmptyList
+import cats.implicits._
+import eu.timepit.refined.types.string.NonEmptyString
+import geotrellis.vector.{io => _, _}
 import io.circe.syntax._
-import cats.syntax.either._
-import cats.syntax.option._
-import geotrellis.vector._
 
 import org.scalatest.funspec.AnyFunSpec
 import org.scalatest.matchers.should.Matchers
@@ -26,41 +28,31 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
               href = "./catalog.json",
               rel = StacLinkType.Self,
               _type = None,
-              title = None,
-              // should it be an optional thing?
-              labelExtAssets = Nil
+              title = None
             ),
             StacLink(
               href = "./catalog.json",
               rel = StacLinkType.StacRoot,
               _type = None,
-              title = None,
-              // should it be an optional thing?
-              labelExtAssets = Nil
+              title = None
             ),
             StacLink(
               href = "./landsat-8-l1/catalog.json",
               rel = StacLinkType.Child,
               _type = None,
-              title = None,
-              // should it be an optional thing?
-              labelExtAssets = Nil
+              title = None
             ),
             StacLink(
               href = "./layers/ca/catalog.json",
               rel = StacLinkType.Child,
               _type = None,
-              title = None,
-              // should it be an optional thing?
-              labelExtAssets = Nil
+              title = None
             ),
             StacLink(
               href = "./layers/us/catalog.json",
               rel = StacLinkType.Child,
               _type = None,
-              title = None,
-              // should it be an optional thing?
-              labelExtAssets = Nil
+              title = None
             )
           )
         )
@@ -81,29 +73,25 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "../catalog.json",
             rel = StacLinkType.StacRoot,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "../catalog.json",
             rel = StacLinkType.Parent,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "./catalog.json",
             rel = StacLinkType.Self,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "../../landsat-8-l1/2014-153/LC81530252014153LGN00.json",
             rel = StacLinkType.Item,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           )
         )
       )
@@ -179,36 +167,34 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
           "landsat:geometric_rmse_model_y"       -> 4.654.asJson,
           "landsat:geometric_rmse_verify"        -> 5.364.asJson,
           "landsat:image_quality_oli"            -> 9.asJson
-        ).asJsonObject.deepMerge(LayerProperties(List(layerUS.id, layerCA.id)).asJsonObject), // layer extension
+        ).asJsonObject.deepMerge(
+          LayerItemExtension(NonEmptyList.of(layerUS.id, layerCA.id) map { NonEmptyString.unsafeFrom }).asJsonObject
+        ), // layer extension
         links = List(
           StacLink(
             href = "../../catalog.json",
             rel = StacLinkType.StacRoot,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "../catalog.json",
             rel = StacLinkType.Parent,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "./LC81530252014153LGN00.json",
             rel = StacLinkType.Self,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           //  { "rel":"alternate", "href": "https://landsatonaws.com/L8/153/025/LC81530252014153LGN00", "type": "text/html"},
           StacLink(
             href = "https://landsatonaws.com/L8/153/025/LC81530252014153LGN0",
             rel = StacLinkType.Alternate,
             _type = `text/html`.some,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           )
         ),
         assets = Map(
@@ -217,21 +203,21 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
               "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_thumb_large.jpg",
             title = "Thumbnail".some,
             description = "A medium sized thumbnail".some,
-            roles = List(StacAssetRole.Thumbnail),
+            roles = Set(StacAssetRole.Thumbnail),
             _type = `image/jpeg`.some
           ),
           "metadata" -> StacItemAsset(
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_MTL.txt",
             title = "Original Metadata".some,
             description = "The original MTL metadata file provided for each Landsat scene".some,
-            roles = List(StacAssetRole.Metadata),
+            roles = Set(StacAssetRole.Metadata),
             _type = VendorMediaType("mtl").some
           ),
           "B1" -> StacItemAsset(
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B1.TIF",
             title = "Coastal Band (B1)".some,
             description = "Coastal Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [0],
           ),
@@ -239,7 +225,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B2.TIF",
             title = "Blue Band (B2)".some,
             description = "Blue Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [1],
           ),
@@ -247,7 +233,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B3.TIF",
             title = "Green Band (B3)".some,
             description = "Green Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [2],
           ),
@@ -255,7 +241,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B4.TIF",
             title = "Red Band (B4)".some,
             description = "Red Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [3],
           ),
@@ -263,7 +249,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B5.TIF",
             title = "NIR Band (B5)".some,
             description = "NIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [4],
           ),
@@ -271,7 +257,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B6.TIF",
             title = "SWIR (B6)".some,
             description = "SWIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [5],
           ),
@@ -279,7 +265,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B7.TIF",
             title = "SWIR Band (B7)".some,
             description = "SWIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [6],
           ),
@@ -287,7 +273,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B8.TIF",
             title = "Panchromatic Band (B8)".some,
             description = "Panchromatic Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [7],
           ),
@@ -295,7 +281,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B9.TIF",
             title = "Cirrus Band (B9)".some,
             description = "Cirrus Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [8],
           ),
@@ -303,7 +289,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B10.TIF",
             title = "LWIR Band (B10)".some,
             description = "LWIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [9],
           ),
@@ -311,7 +297,7 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B11.TIF",
             title = "LWIR Band (B11)".some,
             description = "LWIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [10],
           )
@@ -319,6 +305,9 @@ class CatalogLayerSpec extends AnyFunSpec with Matchers {
         collection = collection.id.some
       )
 
+      ItemExtension[LayerItemExtension].getExtensionFields(item) shouldBe LayerItemExtension(
+        NonEmptyList.fromListUnsafe(List("layer-us", "layer-ca") map { NonEmptyString.unsafeFrom })
+      ).valid
       item.asJson.deepDropNullValues shouldBe getJson(
         "/catalogs/landsat-stac-layers/landsat-8-l1/2014-153/LC81530252014153LGN00.json"
       )
diff --git a/modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala b/modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala
index b9f234af..3c37e4c3 100644
--- a/modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala
+++ b/modules/core/src/test/scala/com/azavea/stac4s/CatalogSpec.scala
@@ -26,25 +26,19 @@ class CatalogSpec extends AnyFunSpec with Matchers {
               href = "./catalog.json",
               rel = StacLinkType.Self,
               _type = None,
-              title = None,
-              // should it be an optional thing?
-              labelExtAssets = Nil
+              title = None
             ),
             StacLink(
               href = "./catalog.json",
               rel = StacLinkType.StacRoot,
               _type = None,
-              title = None,
-              // should it be an optional thing?
-              labelExtAssets = Nil
+              title = None
             ),
             StacLink(
               href = "./landsat-8-l1/catalog.json",
               rel = StacLinkType.Child,
               _type = None,
-              title = None,
-              // should it be an optional thing?
-              labelExtAssets = Nil
+              title = None
             )
           )
         )
@@ -77,6 +71,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             List(TemporalExtent(Instant.parse("2013-06-01T00:56:49.001Z"), Instant.parse("2020-01-01T00:56:49.001Z")))
           )
         ),
+        summaries = ().asJsonObject,
         // properties can be anything
         // it is a part where extensions can be
         // at least EO, Label and potentially the layer extension
@@ -174,29 +169,25 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "../../catalog.json",
             rel = StacLinkType.StacRoot,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "../../catalog.json",
             rel = StacLinkType.Parent,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "./catalog.json",
             rel = StacLinkType.Self,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "./2014-153/LC81530252014153LGN00.json",
             rel = StacLinkType.Item,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           )
         )
       )
@@ -267,30 +258,26 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "../../catalog.json",
             rel = StacLinkType.StacRoot,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "../catalog.json",
             rel = StacLinkType.Parent,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           StacLink(
             href = "./LC81530252014153LGN00.json",
             rel = StacLinkType.Self,
             _type = None,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           ),
           //  { "rel":"alternate", "href": "https://landsatonaws.com/L8/153/025/LC81530252014153LGN00", "type": "text/html"},
           StacLink(
             href = "https://landsatonaws.com/L8/153/025/LC81530252014153LGN0",
             rel = StacLinkType.Alternate,
             _type = `text/html`.some,
-            title = None,
-            labelExtAssets = Nil
+            title = None
           )
         ),
         assets = Map(
@@ -299,21 +286,21 @@ class CatalogSpec extends AnyFunSpec with Matchers {
               "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_thumb_large.jpg",
             title = "Thumbnail".some,
             description = "A medium sized thumbnail".some,
-            roles = List(StacAssetRole.Thumbnail),
+            roles = Set(StacAssetRole.Thumbnail),
             _type = `image/jpeg`.some
           ),
           "metadata" -> StacItemAsset(
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_MTL.txt",
             title = "Original Metadata".some,
             description = "The original MTL metadata file provided for each Landsat scene".some,
-            roles = List(StacAssetRole.Metadata),
+            roles = Set(StacAssetRole.Metadata),
             _type = VendorMediaType("mtl").some
           ),
           "B1" -> StacItemAsset(
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B1.TIF",
             title = "Coastal Band (B1)".some,
             description = "Coastal Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [0],
           ),
@@ -321,7 +308,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B2.TIF",
             title = "Blue Band (B2)".some,
             description = "Blue Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [1],
           ),
@@ -329,7 +316,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B3.TIF",
             title = "Green Band (B3)".some,
             description = "Green Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [2],
           ),
@@ -337,7 +324,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B4.TIF",
             title = "Red Band (B4)".some,
             description = "Red Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [3],
           ),
@@ -345,7 +332,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B5.TIF",
             title = "NIR Band (B5)".some,
             description = "NIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [4],
           ),
@@ -353,7 +340,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B6.TIF",
             title = "SWIR (B6)".some,
             description = "SWIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [5],
           ),
@@ -361,7 +348,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B7.TIF",
             title = "SWIR Band (B7)".some,
             description = "SWIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [6],
           ),
@@ -369,7 +356,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B8.TIF",
             title = "Panchromatic Band (B8)".some,
             description = "Panchromatic Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [7],
           ),
@@ -377,7 +364,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B9.TIF",
             title = "Cirrus Band (B9)".some,
             description = "Cirrus Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [8],
           ),
@@ -385,7 +372,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B10.TIF",
             title = "LWIR Band (B10)".some,
             description = "LWIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [9],
           ),
@@ -393,7 +380,7 @@ class CatalogSpec extends AnyFunSpec with Matchers {
             href = "http://landsat-pds.s3.amazonaws.com/L8/153/025/LC81530252014153LGN00/LC81530252014153LGN00_B11.TIF",
             title = "LWIR Band (B11)".some,
             description = "LWIR Band Top Of the Atmosphere".some,
-            roles = Nil,
+            roles = Set.empty,
             _type = `image/tiff`.some
             // "eo:bands": [10],
           )
diff --git a/modules/core/src/test/scala/com/azavea/stac4s/Generators.scala b/modules/core/src/test/scala/com/azavea/stac4s/Generators.scala
index 9857b9ee..e29fa7a8 100644
--- a/modules/core/src/test/scala/com/azavea/stac4s/Generators.scala
+++ b/modules/core/src/test/scala/com/azavea/stac4s/Generators.scala
@@ -1,10 +1,12 @@
 package com.azavea.stac4s
 
 import com.azavea.stac4s.extensions.label._
+import com.azavea.stac4s.extensions.asset._
 import cats.data.NonEmptyList
 import cats.implicits._
 import geotrellis.vector.{Geometry, Point, Polygon}
 import io.circe.JsonObject
+import io.circe.syntax._
 import org.scalacheck._
 import org.scalacheck.Arbitrary.arbitrary
 import org.scalacheck.cats.implicits._
@@ -13,7 +15,8 @@ import java.time.Instant
 import com.github.tbouron.SpdxLicense
 import com.azavea.stac4s.extensions.label.LabelClassClasses.NamedLabelClasses
 import com.azavea.stac4s.extensions.label.LabelClassClasses.NumberedLabelClasses
-import com.azavea.stac4s.extensions.layer.LayerProperties
+import com.azavea.stac4s.extensions.layer.LayerItemExtension
+import eu.timepit.refined.types.string.NonEmptyString
 
 object Generators {
 
@@ -36,6 +39,35 @@ object Generators {
 
   private def instantGen: Gen[Instant] = arbitrary[Int] map { x => Instant.now.plusMillis(x.toLong) }
 
+  private def assetCollectionExtensionGen: Gen[AssetCollectionExtension] =
+    Gen
+      .mapOf(
+        (nonEmptyStringGen, stacCollectionAssetGen).tupled
+      )
+      .map(AssetCollectionExtension.apply)
+
+  private def collectionExtensionFieldsGen: Gen[JsonObject] = Gen.oneOf(
+    Gen.const(().asJsonObject),
+    assetCollectionExtensionGen.map(_.asJsonObject)
+  )
+
+  private def itemExtensionFieldsGen: Gen[JsonObject] = Gen.oneOf(
+    Gen.const(().asJsonObject),
+    labelExtensionPropertiesGen map { _.asJsonObject },
+    layerPropertiesGen map { _.asJsonObject }
+  )
+
+  private def labelLinkExtensionGen: Gen[LabelLinkExtension] =
+    Gen
+      .nonEmptyListOf(nonEmptyStringGen)
+      .map(NonEmptyList.fromListUnsafe)
+      .map(LabelLinkExtension.apply)
+
+  private def linkExtensionFields: Gen[JsonObject] = Gen.oneOf(
+    Gen.const(().asJsonObject),
+    labelLinkExtensionGen.map(_.asJsonObject)
+  )
+
   private def mediaTypeGen: Gen[StacMediaType] = Gen.oneOf(
     `image/tiff`,
     `image/vnd.stac.geotiff`,
@@ -124,7 +156,7 @@ object Generators {
       Gen.const(StacLinkType.Self), // self link type is required by TMS reification
       Gen.option(mediaTypeGen),
       Gen.option(nonEmptyStringGen),
-      Gen.nonEmptyListOf[String](arbitrary[String])
+      linkExtensionFields
     ).mapN(StacLink.apply)
 
   private def temporalExtentGen: Gen[TemporalExtent] = {
@@ -154,8 +186,9 @@ object Generators {
       nonEmptyStringGen,
       Gen.option(nonEmptyStringGen),
       Gen.option(nonEmptyStringGen),
-      Gen.containerOf[Set, StacAssetRole](assetRoleGen) map { _.toList },
-      Gen.option(mediaTypeGen)
+      Gen.containerOf[Set, StacAssetRole](assetRoleGen),
+      Gen.option(mediaTypeGen),
+      Gen.const(().asJsonObject)
     ) mapN {
       StacItemAsset.apply
     }
@@ -186,7 +219,7 @@ object Generators {
       Gen.nonEmptyListOf(stacLinkGen),
       Gen.nonEmptyMap((nonEmptyStringGen, cogAssetGen).tupled),
       Gen.option(nonEmptyStringGen),
-      Gen.const(JsonObject.fromMap(Map.empty))
+      itemExtensionFieldsGen
     ).mapN(StacItem.apply)
 
   private def stacCatalogGen: Gen[StacCatalog] =
@@ -196,7 +229,8 @@ object Generators {
       nonEmptyStringGen,
       Gen.option(nonEmptyStringGen),
       nonEmptyStringGen,
-      Gen.listOf(stacLinkGen)
+      Gen.listOf(stacLinkGen),
+      Gen.const(().asJsonObject)
     ).mapN(StacCatalog.apply)
 
   private def stacCollectionGen: Gen[StacCollection] =
@@ -210,8 +244,10 @@ object Generators {
       stacLicenseGen,
       Gen.listOf(stacProviderGen),
       stacExtentGen,
+      Gen.const(().asJsonObject),
       Gen.const(JsonObject.fromMap(Map.empty)),
-      Gen.listOf(stacLinkGen)
+      Gen.listOf(stacLinkGen),
+      collectionExtensionFieldsGen
     ).mapN(StacCollection.apply)
 
   private def itemCollectionGen: Gen[ItemCollection] =
@@ -220,7 +256,8 @@ object Generators {
       Gen.const(StacVersion.unsafeFrom("0.9.0")),
       Gen.const(Nil),
       Gen.listOf[StacItem](stacItemGen),
-      Gen.listOf[StacLink](stacLinkGen)
+      Gen.listOf[StacLink](stacLinkGen),
+      Gen.const(().asJsonObject)
     ).mapN(ItemCollection.apply)
 
   private def labelClassNameGen: Gen[LabelClassName] =
@@ -304,10 +341,12 @@ object Generators {
   private def labelPropertiesGen: Gen[LabelProperties] =
     Gen.option(Gen.listOf(nonEmptyStringGen)).map(LabelProperties.fromOption)
 
-  private def layerPropertiesGen: Gen[LayerProperties] =
-    Gen.listOf(nonEmptyStringGen).map(LayerProperties.apply)
+  private def layerPropertiesGen: Gen[LayerItemExtension] =
+    Gen
+      .nonEmptyListOf(nonEmptyStringGen)
+      .map(layerIds => LayerItemExtension(NonEmptyList.fromListUnsafe(layerIds map { NonEmptyString.unsafeFrom })))
 
-  private def labelExtensionPropertiesGen: Gen[LabelExtensionProperties] =
+  private def labelExtensionPropertiesGen: Gen[LabelItemExtension] =
     (
       labelPropertiesGen,
       Gen.listOf(labelClassGen),
@@ -316,7 +355,7 @@ object Generators {
       Gen.listOf(labelTaskGen),
       Gen.listOf(labelMethodGen),
       Gen.listOf(labelOverviewGen)
-    ).mapN(LabelExtensionProperties.apply)
+    ).mapN(LabelItemExtension.apply)
 
   implicit val arbMediaType: Arbitrary[StacMediaType] = Arbitrary {
     mediaTypeGen
@@ -374,6 +413,10 @@ object Generators {
     assetRoleGen
   }
 
+  implicit val arbStacLink: Arbitrary[StacLink] = Arbitrary {
+    stacLinkGen
+  }
+
   implicit val arbLabelClassName: Arbitrary[LabelClassName] = Arbitrary { labelClassNameGen }
 
   implicit val arbLabelClassClasses: Arbitrary[LabelClassClasses] = Arbitrary { labelClassClassesGen }
@@ -394,9 +437,17 @@ object Generators {
 
   implicit val arbLabelProperties: Arbitrary[LabelProperties] = Arbitrary { labelPropertiesGen }
 
-  implicit val arbLabelExtensionProperties: Arbitrary[LabelExtensionProperties] = Arbitrary {
+  implicit val arbLabelExtensionProperties: Arbitrary[LabelItemExtension] = Arbitrary {
     labelExtensionPropertiesGen
   }
 
-  implicit val arbLayerProperties: Arbitrary[LayerProperties] = Arbitrary { layerPropertiesGen }
+  implicit val arbLabelLinkExtension: Arbitrary[LabelLinkExtension] = Arbitrary {
+    labelLinkExtensionGen
+  }
+
+  implicit val arbLayerProperties: Arbitrary[LayerItemExtension] = Arbitrary { layerPropertiesGen }
+
+  implicit val arbAssetExtensionProperties: Arbitrary[AssetCollectionExtension] = Arbitrary {
+    assetCollectionExtensionGen
+  }
 }
diff --git a/modules/core/src/test/scala/com/azavea/stac4s/SerDeSpec.scala b/modules/core/src/test/scala/com/azavea/stac4s/SerDeSpec.scala
index d386d541..0e956d1b 100644
--- a/modules/core/src/test/scala/com/azavea/stac4s/SerDeSpec.scala
+++ b/modules/core/src/test/scala/com/azavea/stac4s/SerDeSpec.scala
@@ -1,9 +1,11 @@
 package com.azavea.stac4s
 
+import com.azavea.stac4s.extensions.asset._
 import com.azavea.stac4s.extensions.label._
 import com.azavea.stac4s.meta._
 import Generators._
 import geotrellis.vector.Geometry
+import io.circe.syntax._
 import io.circe.parser._
 import io.circe.testing.{ArbitraryInstances, CodecTests}
 import org.scalatest.funsuite.AnyFunSuite
@@ -12,7 +14,7 @@ import org.scalatestplus.scalacheck.Checkers
 import org.typelevel.discipline.scalatest.FunSuiteDiscipline
 import java.time.Instant
 
-import com.azavea.stac4s.extensions.layer.LayerProperties
+import com.azavea.stac4s.extensions.layer.LayerItemExtension
 
 class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with Matchers with ArbitraryInstances {
 
@@ -43,7 +45,7 @@ class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with M
   checkAll("Codec.LabelClassClasses", CodecTests[LabelClassClasses].unserializableCodec)
   checkAll("Codec.LabelClassName", CodecTests[LabelClassName].unserializableCodec)
   checkAll("Codec.LabelCount", CodecTests[LabelCount].unserializableCodec)
-  checkAll("Codec.LabelExtensionProperties", CodecTests[LabelExtensionProperties].unserializableCodec)
+  checkAll("Codec.LabelExtensionProperties", CodecTests[LabelItemExtension].unserializableCodec)
   checkAll("Codec.LabelMethod", CodecTests[LabelMethod].unserializableCodec)
   checkAll("Codec.LabelOverview", CodecTests[LabelOverview].unserializableCodec)
   checkAll("Codec.LabelProperties", CodecTests[LabelProperties].unserializableCodec)
@@ -52,12 +54,12 @@ class SerDeSpec extends AnyFunSuite with FunSuiteDiscipline with Checkers with M
   checkAll("Codec.LabelType", CodecTests[LabelType].unserializableCodec)
 
   // Layer extension
-  checkAll("Codec.LayerProperties", CodecTests[LayerProperties].unserializableCodec)
+  checkAll("Codec.LayerProperties", CodecTests[LayerItemExtension].unserializableCodec)
 
   // unit tests
   test("ignore optional fields") {
     val link =
       decode[StacLink]("""{"href":"s3://foo/item.json","rel":"item"}""")
-    link map { _.labelExtAssets } shouldBe Right(List.empty[String])
+    link map { _.extensionFields } shouldBe Right(().asJsonObject)
   }
 }
diff --git a/modules/core/src/test/scala/com/azavea/stac4s/SyntaxSpec.scala b/modules/core/src/test/scala/com/azavea/stac4s/SyntaxSpec.scala
new file mode 100644
index 00000000..7e5368e5
--- /dev/null
+++ b/modules/core/src/test/scala/com/azavea/stac4s/SyntaxSpec.scala
@@ -0,0 +1,60 @@
+package com.azavea.stac4s
+
+import com.azavea.stac4s.syntax._
+import com.azavea.stac4s.extensions._
+import com.azavea.stac4s.extensions.asset._
+import com.azavea.stac4s.extensions.label._
+import Generators._
+import cats.implicits._
+import io.circe.syntax._
+import org.scalatest.funsuite.AnyFunSuite
+import org.scalatest.matchers.should.Matchers
+import org.scalatestplus.scalacheck.Checkers
+
+class SyntaxSpec extends AnyFunSuite with Checkers with Matchers {
+  test("item syntax results in the same values as typeclass summoner to extend") {
+    check { (item: StacItem, labelExtension: LabelItemExtension) =>
+      item.addExtensionFields(labelExtension) == ItemExtension[LabelItemExtension]
+        .addExtensionFields(item, labelExtension)
+    }
+  }
+
+  test("item syntax results in the same values as typeclass summoner to parse") {
+    check { (item: StacItem, labelExtension: LabelItemExtension) =>
+      item.addExtensionFields(labelExtension).getExtensionFields[LabelItemExtension] ==
+        labelExtension.valid
+    }
+  }
+
+  test("collection syntax results in the same values as typeclass summoner to extend") {
+    check { (collection: StacCollection, assetExtension: AssetCollectionExtension) =>
+      collection.addExtensionFields(assetExtension) == CollectionExtension[AssetCollectionExtension]
+        .addExtensionFields(collection, assetExtension)
+    }
+  }
+
+  test("collection syntax results in the same values as typeclass summoner to parse") {
+    check { (collection: StacCollection, assetExtension: AssetCollectionExtension) =>
+      // We have to nuke existing extensions, because otherwise the collection can start with
+      // some assets in its asset extension, in which case the decoded result will have extra
+      // data
+      val withoutExtensions = collection.copy(extensionFields = ().asJsonObject)
+      withoutExtensions
+        .addExtensionFields(assetExtension)
+        .getExtensionFields[AssetCollectionExtension] == assetExtension.valid
+    }
+  }
+
+  test("link syntax results in the same values as typeclass summoner to extend") {
+    check { (stacLink: StacLink, labelLinkExtension: LabelLinkExtension) =>
+      stacLink.addExtensionFields(labelLinkExtension) == LinkExtension[LabelLinkExtension]
+        .addExtensionFields(stacLink, labelLinkExtension)
+    }
+  }
+
+  test("link syntax results in the same values as typeclass summoner to parse") {
+    check { (stacLink: StacLink, labelLinkExtension: LabelLinkExtension) =>
+      stacLink.addExtensionFields(labelLinkExtension).getExtensionFields[LabelLinkExtension] == labelLinkExtension.valid
+    }
+  }
+}