diff --git a/README.md b/README.md
index d0d9342..32e8ae8 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ This SDK supports Featurevisor v3 behavior and v2 datafiles. Generated datafiles
- [Registering modules](#registering-modules)
- [Child instance](#child-instance)
- [Close](#close)
+- [OpenFeature](#openfeature)
- [CLI usage](#cli-usage)
- [Test](#test)
- [Benchmark](#benchmark)
@@ -834,6 +835,50 @@ Learn more about assessing distribution [here](https://featurevisor.com/docs/cli
$ mvn exec:java -Dexec.mainClass="com.featurevisor.cli.CLI" -Dexec.args="assess-distribution --projectDirectoryPath=/absolute/path/to/your/featurevisor/project --environment=production --feature=foo --variation --context='{\"country\": \"nl\"}' --populateUuid=userId --populateUuid=deviceId --n=1000"
```
+## OpenFeature
+
+Add the official OpenFeature SDK next to Featurevisor:
+
+```xml
+
+ dev.openfeature
+ sdk
+ 1.20.2
+
+```
+
+```java
+import com.featurevisor.openfeature.FeaturevisorOpenFeatureProvider;
+import com.featurevisor.sdk.Featurevisor;
+import dev.openfeature.sdk.ImmutableContext;
+import dev.openfeature.sdk.OpenFeatureAPI;
+
+var provider = new FeaturevisorOpenFeatureProvider(
+ new Featurevisor.FeaturevisorOptions().datafile(datafileContent)
+);
+
+var api = OpenFeatureAPI.getInstance();
+api.setProviderAndWait(provider);
+
+var client = api.getClient();
+boolean enabled = client.getBooleanValue("checkout", false, new ImmutableContext("user-123"));
+```
+
+Use `checkout` for a flag, `checkout:variation` for its variation, and `checkout:title` for its `title` variable. Boolean variables use the boolean resolver. Lists, structures, and JSON variables use the object resolver.
+
+OpenFeature's targeting key maps to `userId` by default. `targetingKeyField`, `keySeparator`, and `variationKey` on `FeaturevisorOpenFeatureProvider.Options` can customize the mapping.
+
+You can also reuse an existing Featurevisor instance:
+
+```java
+Featurevisor featurevisor = Featurevisor.createFeaturevisor(featurevisorOptions);
+var provider = new FeaturevisorOpenFeatureProvider(featurevisor);
+```
+
+The caller owns an instance passed this way. Provider shutdown does not close it. Call `featurevisor.close()` when every consumer is finished with it. When the provider creates the instance from options, the provider owns and closes it. If both are configured, the existing instance takes precedence.
+
+See the [OpenFeature provider guide](https://featurevisor.com/docs/sdks/openfeature/) for resolution reasons, errors, metadata, tracking, lifecycle, and providers for other languages.
+
## Development of this package
diff --git a/pom.xml b/pom.xml
index c45a62a..b5b93d3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -101,6 +101,15 @@
commons-exec
1.3
+
+
+
+ dev.openfeature
+ sdk
+ 1.20.2
+ true
+
diff --git a/src/main/java/com/featurevisor/openfeature/FeaturevisorOpenFeatureProvider.java b/src/main/java/com/featurevisor/openfeature/FeaturevisorOpenFeatureProvider.java
new file mode 100644
index 0000000..c282e37
--- /dev/null
+++ b/src/main/java/com/featurevisor/openfeature/FeaturevisorOpenFeatureProvider.java
@@ -0,0 +1,255 @@
+package com.featurevisor.openfeature;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.featurevisor.sdk.Evaluation;
+import com.featurevisor.sdk.Emitter;
+import com.featurevisor.sdk.Featurevisor;
+import com.featurevisor.sdk.FeaturevisorDiagnosticHandler;
+import com.featurevisor.sdk.VariableType;
+import dev.openfeature.sdk.ErrorCode;
+import dev.openfeature.sdk.EvaluationContext;
+import dev.openfeature.sdk.FeatureProvider;
+import dev.openfeature.sdk.ImmutableMetadata;
+import dev.openfeature.sdk.Metadata;
+import dev.openfeature.sdk.ProviderEvaluation;
+import dev.openfeature.sdk.Reason;
+import dev.openfeature.sdk.Structure;
+import dev.openfeature.sdk.TrackingEventDetails;
+import dev.openfeature.sdk.Value;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** OpenFeature provider backed by the Featurevisor v3 SDK. */
+public final class FeaturevisorOpenFeatureProvider implements FeatureProvider {
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ @FunctionalInterface
+ public interface TrackingHandler {
+ void track(String eventName, EvaluationContext context, TrackingEventDetails details);
+ }
+
+ public static final class Options {
+ private Featurevisor featurevisor;
+ private Featurevisor.FeaturevisorOptions featurevisorOptions = new Featurevisor.FeaturevisorOptions();
+ private String targetingKeyField = "userId";
+ private String keySeparator = ":";
+ private String variationKey = "variation";
+ private TrackingHandler onTrack;
+
+ public Options featurevisor(Featurevisor value) { this.featurevisor = value; return this; }
+ public Options featurevisorOptions(Featurevisor.FeaturevisorOptions value) { this.featurevisorOptions = value; return this; }
+ public Options targetingKeyField(String value) { this.targetingKeyField = value; return this; }
+ public Options keySeparator(String value) { this.keySeparator = value; return this; }
+ public Options variationKey(String value) { this.variationKey = value; return this; }
+ public Options onTrack(TrackingHandler value) { this.onTrack = value; return this; }
+ }
+
+ private final Featurevisor featurevisor;
+ private final String targetingKeyField;
+ private final String keySeparator;
+ private final String variationKey;
+ private final TrackingHandler onTrack;
+ private final Emitter.UnsubscribeFunction datafileUnsubscribe;
+ private final boolean ownsFeaturevisor;
+ private String datafileError;
+
+ public FeaturevisorOpenFeatureProvider(Options options) {
+ Options resolved = options != null ? options : new Options();
+ this.targetingKeyField = nonEmpty(resolved.targetingKeyField, "userId");
+ this.keySeparator = nonEmpty(resolved.keySeparator, ":");
+ this.variationKey = nonEmpty(resolved.variationKey, "variation");
+ this.onTrack = resolved.onTrack;
+ this.ownsFeaturevisor = resolved.featurevisor == null;
+ if (resolved.featurevisor != null) {
+ this.featurevisor = resolved.featurevisor;
+ } else {
+ Featurevisor.FeaturevisorOptions fvOptions = resolved.featurevisorOptions != null
+ ? resolved.featurevisorOptions : new Featurevisor.FeaturevisorOptions();
+ if (fvOptions.getDatafileString() != null) {
+ try {
+ com.featurevisor.sdk.DatafileContent.fromJson(fvOptions.getDatafileString());
+ } catch (Exception ignored) {
+ datafileError = "Could not parse datafile";
+ }
+ }
+ FeaturevisorDiagnosticHandler original = fvOptions.getOnDiagnostic();
+ fvOptions.onDiagnostic(diagnostic -> {
+ if ("invalid_datafile".equals(diagnostic.getCode())) datafileError = diagnostic.getMessage();
+ if ("datafile_set".equals(diagnostic.getCode())) datafileError = null;
+ if (original != null) original.handle(diagnostic);
+ });
+ this.featurevisor = Featurevisor.createFeaturevisor(fvOptions);
+ }
+ this.datafileUnsubscribe = this.featurevisor.on(Emitter.EventName.DATAFILE_SET, details -> datafileError = null);
+ }
+
+ public FeaturevisorOpenFeatureProvider(Featurevisor.FeaturevisorOptions options) {
+ this(new Options().featurevisorOptions(options));
+ }
+
+ public FeaturevisorOpenFeatureProvider(Featurevisor featurevisor) {
+ this(new Options().featurevisor(featurevisor));
+ }
+
+ public Featurevisor getFeaturevisor() { return featurevisor; }
+ @Override public Metadata getMetadata() { return () -> "Featurevisor"; }
+ @Override public void shutdown() {
+ datafileUnsubscribe.unsubscribe();
+ if (ownsFeaturevisor) featurevisor.close();
+ }
+ @Override public void track(String name, EvaluationContext context, TrackingEventDetails details) {
+ if (onTrack != null) onTrack.track(name, context, details);
+ }
+
+ @Override public ProviderEvaluation getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext context) {
+ return cast(resolve(key, defaultValue, context, "boolean"), defaultValue, Boolean.class);
+ }
+ @Override public ProviderEvaluation getStringEvaluation(String key, String defaultValue, EvaluationContext context) {
+ return cast(resolve(key, defaultValue, context, "string"), defaultValue, String.class);
+ }
+ @Override public ProviderEvaluation getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext context) {
+ ProviderEvaluation