@@ -44,12 +44,6 @@
gbfs-validator-java
${gbfs-validator.version}
-
-
- org.mobilitydata
- gbfs-validator-java-loader
- ${gbfs-validator.version}
-
diff --git a/example/src/main/java/org/mobilitydata/gbfs/validator/example/GbfsValidatorExample.java b/example/src/main/java/org/mobilitydata/gbfs/validator/example/GbfsValidatorExample.java
index 1366f18d..6d4e1fcc 100644
--- a/example/src/main/java/org/mobilitydata/gbfs/validator/example/GbfsValidatorExample.java
+++ b/example/src/main/java/org/mobilitydata/gbfs/validator/example/GbfsValidatorExample.java
@@ -9,25 +9,21 @@
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import org.mobilitydata.gbfs.validation.GbfsValidator;
import org.mobilitydata.gbfs.validation.GbfsValidatorFactory;
import org.mobilitydata.gbfs.validation.model.FileValidationError;
import org.mobilitydata.gbfs.validation.model.FileValidationResult;
import org.mobilitydata.gbfs.validation.model.ValidationResult;
-import org.mobilitydata.gbfs.validator.loader.LoadedFile;
-import org.mobilitydata.gbfs.validator.loader.Loader;
/**
* Example showing how to validate a GBFS feed using gbfs-validator-java.
*
- * Two use cases are demonstrated:
+ *
This example:
*
- * - Single-file validation via {@link GbfsValidator#validateFile}
- * - Full-feed validation via {@link Loader} + {@link GbfsValidator#validate}:
- * the Loader fetches gbfs.json and automatically discovers and loads all
- * linked feed files, so no manual URL construction is needed.
+ * - Fetches gbfs.json from a public GBFS feed
+ * - Validates the file using GbfsValidatorFactory
+ * - Prints the validation results to stdout
*
*
* Usage:
@@ -46,54 +42,57 @@ public class GbfsValidatorExample {
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("=== GBFS Validator Java Example ===\n");
- GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator();
-
// --- Example 1: Validate a single file ---
- // Useful when you already have the file content and just want schema validation.
System.out.println("Example 1: Validate a single file");
System.out.println("Fetching: " + GBFS_FEED_URL);
- String gbfsContent = fetchUrl(GBFS_FEED_URL);
- InputStream gbfsStream = new ByteArrayInputStream(gbfsContent.getBytes(StandardCharsets.UTF_8));
- FileValidationResult fileResult = validator.validateFile("gbfs", gbfsStream);
+ String fileContents = fetchUrl(GBFS_FEED_URL);
+
+ GbfsValidator validator = GbfsValidatorFactory.getGbfsJsonValidator();
+ InputStream fileStream = new ByteArrayInputStream(
+ fileContents.getBytes(StandardCharsets.UTF_8)
+ );
+
+ // The API expects file names WITHOUT the .json extension (e.g. "gbfs", not "gbfs.json")
+ FileValidationResult fileResult = validator.validateFile("gbfs", fileStream);
printFileResult(fileResult);
- // --- Example 2: Validate a full feed ---
- // The Loader fetches gbfs.json, parses the feed URLs from its discovery data,
- // and loads all linked files — handling language prefixes and auth automatically.
+ // --- Example 2: Validate a full feed (multiple files) ---
System.out.println("\nExample 2: Validate a full feed");
- Loader loader = new Loader();
- try {
- List loadedFiles = loader.load(GBFS_FEED_URL);
-
- Map fileMap = new HashMap<>();
- for (LoadedFile file : loadedFiles) {
- // Keep the discovery file (no language) and only "en" language files.
- // If a feed does not publish "en", swap "en" for the desired language code.
- String lang = file.language();
- if (lang != null && !lang.equals("en")) {
- continue;
- }
- if (file.fileContents() != null) {
- System.out.println(" Loaded: " + file.fileName() + " (" + file.url() + ")");
- fileMap.put(file.fileName(), file.fileContents());
- } else {
- file.loaderErrors().forEach(e ->
- System.out.println(" Skipped: " + file.fileName()
- + " (" + e.error() + ": " + e.message() + ")")
- );
- }
+ Map feedFiles = new HashMap<>();
+
+ // Keys must be the GBFS file type name (no .json extension)
+ feedFiles.put("gbfs", new ByteArrayInputStream(
+ fileContents.getBytes(StandardCharsets.UTF_8)
+ ));
+
+ // Fetch additional files — URL uses .json, but map key does not
+ String[] additionalFileNames = {
+ "system_information",
+ "station_information",
+ "station_status",
+ "free_bike_status",
+ };
+ String baseUrl = GBFS_FEED_URL.substring(0, GBFS_FEED_URL.lastIndexOf('/') + 1);
+ for (String fileType : additionalFileNames) {
+ try {
+ String content = fetchUrl(baseUrl + fileType + ".json");
+ feedFiles.put(fileType, new ByteArrayInputStream(
+ content.getBytes(StandardCharsets.UTF_8)
+ ));
+ System.out.println(" Loaded: " + fileType);
+ } catch (Exception e) {
+ System.out.println(" Skipped: " + fileType + " (" + e.getMessage() + ")");
}
-
- ValidationResult feedResult = validator.validate(fileMap);
- printFeedResult(feedResult);
- } finally {
- loader.close();
}
+
+ ValidationResult feedResult = validator.validate(feedFiles);
+ printFeedResult(feedResult);
}
private static void printFileResult(FileValidationResult result) {
System.out.println(" File : " + result.file());
System.out.println(" Version : " + result.version());
+ System.out.println(" Schema : " + result.schema());
System.out.println(" Exists : " + result.exists());
System.out.println(" Required: " + result.required());
System.out.println(" Errors : " + result.errorsCount());
@@ -116,17 +115,14 @@ private static void printFileResult(FileValidationResult result) {
private static void printFeedResult(ValidationResult result) {
System.out.println(" Summary : " + result.summary());
- var presentFiles = result.files().entrySet().stream()
- .filter(e -> e.getValue().exists())
- .toList();
- System.out.println(" Files validated: " + presentFiles.size());
- presentFiles.forEach(e -> {
+ System.out.println(" Files validated: " + result.files().size());
+ result.files().forEach((name, fileResult) -> {
System.out.printf(" %-35s errors=%d version=%s%n",
- e.getKey(), e.getValue().errorsCount(), e.getValue().version());
+ name, fileResult.errorsCount(), fileResult.version());
});
- long totalErrors = presentFiles.stream()
- .mapToLong(e -> e.getValue().errorsCount())
+ long totalErrors = result.files().values().stream()
+ .mapToLong(FileValidationResult::errorsCount)
.sum();
System.out.println("\n Total errors across all files: " + totalErrors);
@@ -143,11 +139,12 @@ private static String fetchUrl(String url) throws IOException, InterruptedExcept
.uri(URI.create(url))
.GET()
.build();
- HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ HttpResponse response = client.send(
+ request, HttpResponse.BodyHandlers.ofString()
+ );
if (response.statusCode() != 200) {
throw new IOException("HTTP " + response.statusCode() + " for " + url);
}
return response.body();
}
}
-
diff --git a/gbfs-validator-java-cli/src/main/java/org/mobilitydata/gbfs/validator/cli/formatter/ConsoleReportFormatter.java b/gbfs-validator-java-cli/src/main/java/org/mobilitydata/gbfs/validator/cli/formatter/ConsoleReportFormatter.java
index 814266ae..d5549c22 100644
--- a/gbfs-validator-java-cli/src/main/java/org/mobilitydata/gbfs/validator/cli/formatter/ConsoleReportFormatter.java
+++ b/gbfs-validator-java-cli/src/main/java/org/mobilitydata/gbfs/validator/cli/formatter/ConsoleReportFormatter.java
@@ -82,8 +82,6 @@ public String format(
if (fileResult.required()) {
sb.append(" [REQUIRED]");
- } else if (fileResult.conditionallyRequired()) {
- sb.append(" [CONDITIONALLY REQUIRED]");
}
if (!fileResult.exists()) {
diff --git a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/model/FileValidationResult.java b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/model/FileValidationResult.java
index 42f751ca..4273ecf6 100644
--- a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/model/FileValidationResult.java
+++ b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/model/FileValidationResult.java
@@ -27,8 +27,6 @@
* The result of validating a single GBFS file
* @param file The name of the file that was validated
* @param required Whether the file is required in the given version of GBFS
- * @param conditionallyRequired Whether the file is conditionally required (e.g. station_status or
- * vehicle_status must be present — neither is individually required, but at least one must exist)
* @param exists Whether the file existed in the validation input
* @param errorsCount The number of errors found while validating the file
* @param schema The schema used to validate the file
@@ -40,7 +38,6 @@
public record FileValidationResult(
String file,
boolean required,
- boolean conditionallyRequired,
boolean exists,
int errorsCount,
String schema,
@@ -64,8 +61,6 @@ public String toString() {
'\'' +
", required=" +
required +
- ", conditionallyRequired=" +
- conditionallyRequired +
", exists=" +
exists +
", errorsCount=" +
@@ -91,7 +86,6 @@ public String toString() {
public boolean sameAs(FileValidationResult other) {
if (other == null) return false;
if (required != other.required) return false;
- if (conditionallyRequired != other.conditionallyRequired) return false;
if (exists != other.exists) return false;
if (errorsCount != other.errorsCount) return false; // This should ideally reflect both validation and system errors count
if (!Objects.equals(file, other.file)) return false;
diff --git a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/FileValidator.java b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/FileValidator.java
index bb88a172..96098728 100644
--- a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/FileValidator.java
+++ b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/FileValidator.java
@@ -80,14 +80,13 @@ public FileValidationResult validate(
return new FileValidationResult(
feedName,
isRequired(feedName),
- false,
feed != null,
errorsCount,
schema.toString(),
Optional.ofNullable(feed).map(JSONObject::toString).orElse(null),
version.getVersionString(),
validationErrors,
- java.util.Collections.emptyList()
+ java.util.Collections.emptyList() // Added for systemErrors
);
}
@@ -131,7 +130,6 @@ public FileValidationResult validateMissingFile(String file) {
file,
isRequired,
false,
- false,
isRequired ? 1 : 0,
version.getSchema(file).toString(),
null,
diff --git a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java
index 33c220c8..db813273 100644
--- a/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java
+++ b/gbfs-validator-java/src/main/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidator.java
@@ -131,7 +131,6 @@ public ValidationResult validate(Map rawFeeds) {
List missingFiles = findMissingFiles(version, fileValidations);
handleMissingFiles(fileValidations, missingFiles, version); // This creates FVRs for missing files
- checkStatusFilePresence(fileValidations, version);
ValidationSummary summary = new ValidationSummary(
version.getVersionString(),
@@ -148,60 +147,6 @@ public ValidationResult validate(Map rawFeeds) {
return new ValidationResult(summary, fileValidations);
}
- /**
- * Per GBFS spec, a feed must be dock-based, free-floating, or hybrid, so at least one of
- * station_status / vehicle_status (v3+) or station_status / free_bike_status (pre-v3) must
- * be present. If neither is present, both are flagged as required with an error.
- */
- private void checkStatusFilePresence(
- Map fileValidations,
- Version version
- ) {
- String freeFloatingFile = version.getFileNames().contains("vehicle_status")
- ? "vehicle_status"
- : "free_bike_status";
-
- boolean stationStatusAbsent = !isPresent(fileValidations, "station_status");
- boolean freeFloatingAbsent = !isPresent(fileValidations, freeFloatingFile);
-
- if (stationStatusAbsent && freeFloatingAbsent) {
- markAsConditionallyRequired(fileValidations, "station_status");
- markAsConditionallyRequired(fileValidations, freeFloatingFile);
- }
- }
-
- private boolean isPresent(
- Map fileValidations,
- String file
- ) {
- FileValidationResult result = fileValidations.get(file);
- return result != null && result.exists();
- }
-
- private void markAsConditionallyRequired(
- Map fileValidations,
- String file
- ) {
- FileValidationResult existing = fileValidations.get(file);
- if (existing != null) {
- fileValidations.put(
- file,
- new FileValidationResult(
- existing.file(),
- false,
- true,
- false,
- 1,
- existing.schema(),
- null,
- existing.version(),
- Collections.emptyList(),
- Collections.emptyList()
- )
- );
- }
- }
-
private Version detectVersionFromParsedFeeds(
Map parsedFeeds
) {
@@ -412,7 +357,6 @@ private FileValidationResult createParsingErrorResult(
return new FileValidationResult(
feedName,
supportedFeed && schemaVersion.isFileRequired(feedName),
- false,
true,
0,
supportedFeed ? schemaVersion.getSchema(feedName).toString() : null,
diff --git a/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/model/ValidationResultTest.java b/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/model/ValidationResultTest.java
index 6829a928..a3ebcb99 100644
--- a/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/model/ValidationResultTest.java
+++ b/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/model/ValidationResultTest.java
@@ -64,7 +64,6 @@ private FileValidationResult generateFileValidationResult(
return new FileValidationResult(
"gbfs",
true,
- false,
true,
2,
null,
diff --git a/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidatorTest.java b/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidatorTest.java
index 5bc8d8d7..b9e601cf 100644
--- a/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidatorTest.java
+++ b/gbfs-validator-java/src/test/java/org/mobilitydata/gbfs/validation/validator/GbfsJsonValidatorTest.java
@@ -40,9 +40,9 @@ void testEmptyDeliveryMapValidation() {
Map deliveryMap = new HashMap<>();
ValidationResult result = validator.validate(deliveryMap);
- // The expected error count is 4: gbfs.json, system_information.json, station_status.json,
- // and free_bike_status.json are all required (directly or conditionally) but missing.
- Assertions.assertEquals(4, result.summary().errorsCount());
+ // The expected error count is 2, because there are two required files
+ // missing in an empty delivery, gbfs.json, and system_information.json
+ Assertions.assertEquals(2, result.summary().errorsCount());
}
@Test
@@ -59,10 +59,6 @@ void testSuccessfulV1_0Validation() {
"system_hours",
getFixture("fixtures/v1.0/system_hours.json")
);
- deliveryMap.put(
- "free_bike_status",
- getFixture("fixtures/v1.0/free_bike_status.json")
- );
ValidationResult result = validator.validate(deliveryMap);
@@ -90,10 +86,6 @@ void testSuccessfulV1_1Validation() {
"system_hours",
getFixture("fixtures/v1.1/system_hours.json")
);
- deliveryMap.put(
- "free_bike_status",
- getFixture("fixtures/v1.1/free_bike_status.json")
- );
ValidationResult result = validator.validate(deliveryMap);
@@ -121,10 +113,6 @@ void testSuccessfulV2_0Validation() {
"system_hours",
getFixture("fixtures/v2.0/system_hours.json")
);
- deliveryMap.put(
- "free_bike_status",
- getFixture("fixtures/v2.0/free_bike_status.json")
- );
ValidationResult result = validator.validate(deliveryMap);
@@ -487,8 +475,7 @@ void testMissingRequiredFile() {
Assertions.assertTrue(result.files().get("system_information").required());
Assertions.assertFalse(result.files().get("system_information").exists());
- // 3 errors: system_information, station_status, and free_bike_status are all missing
- Assertions.assertEquals(3, result.summary().errorsCount());
+ Assertions.assertEquals(1, result.summary().errorsCount());
}
@Test
@@ -517,16 +504,6 @@ void testMissingNotRequiredFile() {
"system_information",
getFixture("fixtures/v2.2/system_information.json")
);
- // Minimal station_status (empty stations) — satisfies the conditional requirement
- // without triggering custom-rule violations that depend on other files.
- String minimalStationStatus =
- "{\"last_updated\":1609866235,\"ttl\":0,\"version\":\"2.2\",\"data\":{\"stations\":[]}}";
- deliveryMap.put(
- "station_status",
- new ByteArrayInputStream(
- minimalStationStatus.getBytes(java.nio.charset.StandardCharsets.UTF_8)
- )
- );
ValidationResult result = validator.validate(deliveryMap);
@@ -652,128 +629,6 @@ void testReadError() throws IOException {
);
}
- @Test
- void testMissingBothStatusFilesV3IsInvalid() {
- GbfsJsonValidator validator = new GbfsJsonValidator();
-
- // v3.0 feed with neither station_status nor vehicle_status
- Map deliveryMap = new HashMap<>();
- deliveryMap.put("gbfs", getFixture("fixtures/v3.0/gbfs.json"));
- deliveryMap.put(
- "system_information",
- getFixture("fixtures/v3.0/system_information.json")
- );
-
- ValidationResult result = validator.validate(deliveryMap);
-
- assertTrue(
- result.summary().errorsCount() > 0,
- "Feed missing both station_status and vehicle_status should be invalid"
- );
-
- FileValidationResult stationStatus = result.files().get("station_status");
- FileValidationResult vehicleStatus = result.files().get("vehicle_status");
-
- assertNotNull(stationStatus);
- assertNotNull(vehicleStatus);
- assertTrue(
- stationStatus.conditionallyRequired(),
- "station_status should be marked conditionallyRequired when vehicle_status is also absent"
- );
- assertTrue(
- vehicleStatus.conditionallyRequired(),
- "vehicle_status should be marked conditionallyRequired when station_status is also absent"
- );
- assertEquals(1, stationStatus.errorsCount());
- assertEquals(1, vehicleStatus.errorsCount());
- }
-
- @Test
- void testMissingBothStatusFilesPreV3IsInvalid() {
- GbfsJsonValidator validator = new GbfsJsonValidator();
-
- // v2.3 feed with neither station_status nor free_bike_status
- Map deliveryMap = new HashMap<>();
- deliveryMap.put("gbfs", getFixture("fixtures/v2.3/gbfs.json"));
- deliveryMap.put(
- "system_information",
- getFixture("fixtures/v2.3/system_information.json")
- );
-
- ValidationResult result = validator.validate(deliveryMap);
-
- assertTrue(
- result.summary().errorsCount() > 0,
- "Feed missing both station_status and free_bike_status should be invalid"
- );
-
- FileValidationResult stationStatus = result.files().get("station_status");
- FileValidationResult freeBikeStatus = result
- .files()
- .get("free_bike_status");
-
- assertNotNull(stationStatus);
- assertNotNull(freeBikeStatus);
- assertTrue(stationStatus.conditionallyRequired());
- assertTrue(freeBikeStatus.conditionallyRequired());
- assertEquals(1, stationStatus.errorsCount());
- assertEquals(1, freeBikeStatus.errorsCount());
- }
-
- @Test
- void testOnlyStationStatusPresentV3IsValid() {
- GbfsJsonValidator validator = new GbfsJsonValidator();
-
- // v3.0 dock-based feed — vehicle_status absent, but station_status present
- Map deliveryMap = new HashMap<>();
- deliveryMap.put("gbfs", getFixture("fixtures/v3.0/gbfs.json"));
- deliveryMap.put(
- "system_information",
- getFixture("fixtures/v3.0/system_information.json")
- );
- deliveryMap.put(
- "station_status",
- getFixture("fixtures/v3.0/station_status.json")
- );
-
- ValidationResult result = validator.validate(deliveryMap);
-
- FileValidationResult vehicleStatus = result.files().get("vehicle_status");
- assertNotNull(vehicleStatus);
- assertFalse(
- vehicleStatus.required(),
- "vehicle_status should not be required when station_status is present"
- );
- assertEquals(0, vehicleStatus.errorsCount());
- }
-
- @Test
- void testOnlyVehicleStatusPresentV3IsValid() {
- GbfsJsonValidator validator = new GbfsJsonValidator();
-
- // v3.0 free-floating feed — station_status absent, but vehicle_status present
- Map deliveryMap = new HashMap<>();
- deliveryMap.put("gbfs", getFixture("fixtures/v3.0/gbfs.json"));
- deliveryMap.put(
- "system_information",
- getFixture("fixtures/v3.0/system_information.json")
- );
- deliveryMap.put(
- "vehicle_status",
- getFixture("fixtures/v3.0/vehicle_status.json")
- );
-
- ValidationResult result = validator.validate(deliveryMap);
-
- FileValidationResult stationStatus = result.files().get("station_status");
- assertNotNull(stationStatus);
- assertFalse(
- stationStatus.required(),
- "station_status should not be required when vehicle_status is present"
- );
- assertEquals(0, stationStatus.errorsCount());
- }
-
// Helper class for testing IOException during read
private static class ThrowingInputStream extends InputStream {
diff --git a/gbfs-validator-java/src/test/resources/fixtures/v1.0/free_bike_status.json b/gbfs-validator-java/src/test/resources/fixtures/v1.0/free_bike_status.json
deleted file mode 100644
index 1a45cc4e..00000000
--- a/gbfs-validator-java/src/test/resources/fixtures/v1.0/free_bike_status.json
+++ /dev/null
@@ -1 +0,0 @@
-{"last_updated":1609866235,"ttl":0,"data":{"bikes":[]}}
diff --git a/gbfs-validator-java/src/test/resources/fixtures/v1.1/free_bike_status.json b/gbfs-validator-java/src/test/resources/fixtures/v1.1/free_bike_status.json
deleted file mode 100644
index e8c3612e..00000000
--- a/gbfs-validator-java/src/test/resources/fixtures/v1.1/free_bike_status.json
+++ /dev/null
@@ -1 +0,0 @@
-{"last_updated":1609866235,"ttl":0,"version":"1.1","data":{"bikes":[]}}
diff --git a/gbfs-validator-java/src/test/resources/fixtures/v2.0/free_bike_status.json b/gbfs-validator-java/src/test/resources/fixtures/v2.0/free_bike_status.json
deleted file mode 100644
index 4c59714f..00000000
--- a/gbfs-validator-java/src/test/resources/fixtures/v2.0/free_bike_status.json
+++ /dev/null
@@ -1 +0,0 @@
-{"last_updated":1609866235,"ttl":0,"version":"2.0","data":{"bikes":[]}}