From 2373b878c8cfcc7f1ced4de1c7e460697980a7ee Mon Sep 17 00:00:00 2001 From: jcpitre Date: Mon, 13 Jul 2026 17:05:49 -0400 Subject: [PATCH 1/3] Modified example to use Loader for feed --- example/pom.xml | 8 +- .../example/GbfsValidatorExample.java | 103 +++++++++--------- 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/example/pom.xml b/example/pom.xml index cd31436..1c0c1d0 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -20,7 +20,7 @@ 17 UTF-8 - 3.0.3 + 3.1.0 @@ -44,6 +44,12 @@ 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 6d4e1fc..1366f18 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,21 +9,25 @@ 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. * - *

This example: + *

Two use cases are demonstrated: *

    - *
  1. Fetches gbfs.json from a public GBFS feed
  2. - *
  3. Validates the file using GbfsValidatorFactory
  4. - *
  5. Prints the validation results to stdout
  6. + *
  7. Single-file validation via {@link GbfsValidator#validateFile}
  8. + *
  9. 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.
  10. *
* *

Usage: @@ -42,57 +46,54 @@ 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 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); + String gbfsContent = fetchUrl(GBFS_FEED_URL); + InputStream gbfsStream = new ByteArrayInputStream(gbfsContent.getBytes(StandardCharsets.UTF_8)); + FileValidationResult fileResult = validator.validateFile("gbfs", gbfsStream); printFileResult(fileResult); - // --- Example 2: Validate a full feed (multiple files) --- + // --- 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. System.out.println("\nExample 2: Validate a full feed"); - 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() + ")"); + 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() + ")") + ); + } } - } - ValidationResult feedResult = validator.validate(feedFiles); - printFeedResult(feedResult); + ValidationResult feedResult = validator.validate(fileMap); + printFeedResult(feedResult); + } finally { + loader.close(); + } } 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()); @@ -115,14 +116,17 @@ private static void printFileResult(FileValidationResult result) { private static void printFeedResult(ValidationResult result) { System.out.println(" Summary : " + result.summary()); - System.out.println(" Files validated: " + result.files().size()); - result.files().forEach((name, fileResult) -> { + var presentFiles = result.files().entrySet().stream() + .filter(e -> e.getValue().exists()) + .toList(); + System.out.println(" Files validated: " + presentFiles.size()); + presentFiles.forEach(e -> { System.out.printf(" %-35s errors=%d version=%s%n", - name, fileResult.errorsCount(), fileResult.version()); + e.getKey(), e.getValue().errorsCount(), e.getValue().version()); }); - long totalErrors = result.files().values().stream() - .mapToLong(FileValidationResult::errorsCount) + long totalErrors = presentFiles.stream() + .mapToLong(e -> e.getValue().errorsCount()) .sum(); System.out.println("\n Total errors across all files: " + totalErrors); @@ -139,12 +143,11 @@ 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(); } } + From 078216a88da07483a21a7eca73ecf7a4d4f7b804 Mon Sep 17 00:00:00 2001 From: jcpitre Date: Tue, 14 Jul 2026 09:48:38 -0400 Subject: [PATCH 2/3] fix: flag feed as invalid when both station_status and vehicle_status are missing --- .../model/FileValidationResult.java | 6 + .../validation/validator/FileValidator.java | 4 +- .../validator/GbfsJsonValidator.java | 56 +++++++ .../model/ValidationResultTest.java | 1 + .../validator/GbfsJsonValidatorTest.java | 153 +++++++++++++++++- 5 files changed, 215 insertions(+), 5 deletions(-) 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 4273ecf..42f751c 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,6 +27,8 @@ * 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 @@ -38,6 +40,7 @@ public record FileValidationResult( String file, boolean required, + boolean conditionallyRequired, boolean exists, int errorsCount, String schema, @@ -61,6 +64,8 @@ public String toString() { '\'' + ", required=" + required + + ", conditionallyRequired=" + + conditionallyRequired + ", exists=" + exists + ", errorsCount=" + @@ -86,6 +91,7 @@ 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 9609872..bb88a17 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,13 +80,14 @@ 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() // Added for systemErrors + java.util.Collections.emptyList() ); } @@ -130,6 +131,7 @@ 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 db81327..33c220c 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,6 +131,7 @@ 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(), @@ -147,6 +148,60 @@ 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 ) { @@ -357,6 +412,7 @@ 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 a3ebcb9..6829a92 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,6 +64,7 @@ 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 b9e601c..5bc8d8d 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 2, because there are two required files - // missing in an empty delivery, gbfs.json, and system_information.json - Assertions.assertEquals(2, result.summary().errorsCount()); + // 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()); } @Test @@ -59,6 +59,10 @@ 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); @@ -86,6 +90,10 @@ 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); @@ -113,6 +121,10 @@ 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); @@ -475,7 +487,8 @@ void testMissingRequiredFile() { Assertions.assertTrue(result.files().get("system_information").required()); Assertions.assertFalse(result.files().get("system_information").exists()); - Assertions.assertEquals(1, result.summary().errorsCount()); + // 3 errors: system_information, station_status, and free_bike_status are all missing + Assertions.assertEquals(3, result.summary().errorsCount()); } @Test @@ -504,6 +517,16 @@ 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); @@ -629,6 +652,128 @@ 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 { From abd2fc54d699574d013b3caf980bbabf43cb7037 Mon Sep 17 00:00:00 2001 From: jcpitre Date: Tue, 14 Jul 2026 10:05:04 -0400 Subject: [PATCH 3/3] Added missing files --- .../gbfs/validator/cli/formatter/ConsoleReportFormatter.java | 2 ++ .../src/test/resources/fixtures/v1.0/free_bike_status.json | 1 + .../src/test/resources/fixtures/v1.1/free_bike_status.json | 1 + .../src/test/resources/fixtures/v2.0/free_bike_status.json | 1 + 4 files changed, 5 insertions(+) create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v1.0/free_bike_status.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v1.1/free_bike_status.json create mode 100644 gbfs-validator-java/src/test/resources/fixtures/v2.0/free_bike_status.json 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 d5549c2..814266a 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,6 +82,8 @@ 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/test/resources/fixtures/v1.0/free_bike_status.json b/gbfs-validator-java/src/test/resources/fixtures/v1.0/free_bike_status.json new file mode 100644 index 0000000..1a45cc4 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v1.0/free_bike_status.json @@ -0,0 +1 @@ +{"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 new file mode 100644 index 0000000..e8c3612 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v1.1/free_bike_status.json @@ -0,0 +1 @@ +{"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 new file mode 100644 index 0000000..4c59714 --- /dev/null +++ b/gbfs-validator-java/src/test/resources/fixtures/v2.0/free_bike_status.json @@ -0,0 +1 @@ +{"last_updated":1609866235,"ttl":0,"version":"2.0","data":{"bikes":[]}}