diff --git a/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/api/datagen/v1/provider/FabricCodecDataProvider.java b/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/api/datagen/v1/provider/FabricCodecDataProvider.java
new file mode 100644
index 000000000..86190079b
--- /dev/null
+++ b/fabric-data-generation-api-v1/src/main/java/net/fabricmc/fabric/api/datagen/v1/provider/FabricCodecDataProvider.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.api.datagen.v1.provider;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.BiConsumer;
+
+import com.google.gson.JsonElement;
+import com.mojang.serialization.Codec;
+import com.mojang.serialization.DataResult;
+import com.mojang.serialization.JsonOps;
+
+import net.minecraft.data.DataOutput;
+import net.minecraft.data.DataProvider;
+import net.minecraft.data.DataWriter;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
+import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
+
+/**
+ * Extend this class and implement {@link FabricCodecDataProvider#configure}.
+ *
+ * <p>Register an instance of the class with {@link FabricDataGenerator.Pack#addProvider} in a {@link net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint}.
+ */
+public abstract class FabricCodecDataProvider<T> implements DataProvider {
+	private final DataOutput.PathResolver pathResolver;
+	private final Codec<T> codec;
+
+	protected FabricCodecDataProvider(FabricDataOutput dataOutput, DataOutput.OutputType outputType, String directoryName, Codec<T> codec) {
+		this.pathResolver = dataOutput.getResolver(outputType, directoryName);
+		this.codec = codec;
+	}
+
+	@Override
+	public CompletableFuture<?> run(DataWriter writer) {
+		Map<Identifier, JsonElement> entries = new HashMap<>();
+		BiConsumer<Identifier, T> provider = (id, value) -> {
+			JsonElement json = this.convert(id, value);
+			JsonElement existingJson = entries.put(id, json);
+
+			if (existingJson != null) {
+				throw new IllegalArgumentException("Duplicate entry " + id);
+			}
+		};
+
+		this.configure(provider);
+		return this.write(writer, entries);
+	}
+
+	/**
+	 * Implement this method to register entries to generate.
+	 *
+	 * @param provider A consumer that accepts an {@link Identifier} and a value to register.
+	 */
+	protected abstract void configure(BiConsumer<Identifier, T> provider);
+
+	private JsonElement convert(Identifier id, T value) {
+		DataResult<JsonElement> dataResult = this.codec.encodeStart(JsonOps.INSTANCE, value);
+		return dataResult.get()
+				.mapRight(partial -> "Invalid entry %s: %s".formatted(id, partial.message()))
+				.orThrow();
+	}
+
+	private CompletableFuture<?> write(DataWriter writer, Map<Identifier, JsonElement> entries) {
+		return CompletableFuture.allOf(entries.entrySet().stream().map(entry -> {
+			Path path = this.pathResolver.resolveJson(entry.getKey());
+			return DataProvider.writeToPath(writer, entry.getValue(), path);
+		}).toArray(CompletableFuture[]::new));
+	}
+}
diff --git a/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java b/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java
index 13491c112..bb63ccfe1 100644
--- a/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java
+++ b/fabric-data-generation-api-v1/src/testmod/java/net/fabricmc/fabric/test/datagen/DataGeneratorTestEntrypoint.java
@@ -26,6 +26,7 @@ import static net.fabricmc.fabric.test.datagen.DataGeneratorTestContent.SIMPLE_I
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.BiConsumer;
@@ -38,7 +39,11 @@ import net.minecraft.advancement.Advancement;
 import net.minecraft.advancement.AdvancementFrame;
 import net.minecraft.advancement.criterion.OnKilledCriterion;
 import net.minecraft.block.Blocks;
+import net.minecraft.client.texture.atlas.AtlasSource;
+import net.minecraft.client.texture.atlas.AtlasSourceManager;
+import net.minecraft.client.texture.atlas.DirectoryAtlasSource;
 import net.minecraft.registry.RegistryKeys;
+import net.minecraft.data.DataOutput;
 import net.minecraft.data.client.BlockStateModelGenerator;
 import net.minecraft.data.client.ItemModelGenerator;
 import net.minecraft.data.server.recipe.RecipeJsonProvider;
@@ -69,6 +74,7 @@ import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator;
 import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput;
 import net.fabricmc.fabric.api.datagen.v1.provider.FabricAdvancementProvider;
 import net.fabricmc.fabric.api.datagen.v1.provider.FabricBlockLootTableProvider;
+import net.fabricmc.fabric.api.datagen.v1.provider.FabricCodecDataProvider;
 import net.fabricmc.fabric.api.datagen.v1.provider.FabricLanguageProvider;
 import net.fabricmc.fabric.api.datagen.v1.provider.FabricModelProvider;
 import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider;
@@ -98,6 +104,7 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
 		TestBlockTagProvider blockTagProvider = pack.addProvider(TestBlockTagProvider::new);
 		pack.addProvider((output, registries) -> new TestItemTagProvider(output, registries, blockTagProvider));
 		pack.addProvider(TestBiomeTagProvider::new);
+		pack.addProvider(TestAtlasSourceProvider::new);
 	}
 
 	private static class TestRecipeProvider extends FabricRecipeProvider {
@@ -350,4 +357,20 @@ public class DataGeneratorTestEntrypoint implements DataGeneratorEntrypoint {
 			);
 		}
 	}
+
+	private static class TestAtlasSourceProvider extends FabricCodecDataProvider<List<AtlasSource>> {
+		private TestAtlasSourceProvider(FabricDataOutput dataOutput) {
+			super(dataOutput, DataOutput.OutputType.RESOURCE_PACK, "atlases", AtlasSourceManager.LIST_CODEC);
+		}
+
+		@Override
+		protected void configure(BiConsumer<Identifier, List<AtlasSource>> provider) {
+			provider.accept(new Identifier(MOD_ID, "atlas_source_test"), List.of(new DirectoryAtlasSource("example", "example/")));
+		}
+
+		@Override
+		public String getName() {
+			return "Atlas Sources";
+		}
+	}
 }