{"id":16352341,"url":"https://github.com/melbournedeveloper/jayse","last_synced_at":"2025-03-16T15:31:50.677Z","repository":{"id":91838290,"uuid":"380050463","full_name":"MelbourneDeveloper/Jayse","owner":"MelbourneDeveloper","description":"Lossless conversion of JSON to and from statically-typed, immutable objects in Dart and .NET","archived":false,"fork":false,"pushed_at":"2024-05-18T04:20:54.000Z","size":228,"stargazers_count":30,"open_issues_count":3,"forks_count":2,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-03-11T11:10:02.130Z","etag":null,"topics":["dart","flutter","immutable","json"],"latest_commit_sha":null,"homepage":"https://pub.dev/packages/jayse","language":"Dart","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MelbourneDeveloper.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-06-24T21:06:19.000Z","updated_at":"2024-12-24T07:50:35.000Z","dependencies_parsed_at":null,"dependency_job_id":"dea86f4a-cda9-4740-a14a-f4d71a81c54d","html_url":"https://github.com/MelbourneDeveloper/Jayse","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MelbourneDeveloper%2FJayse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MelbourneDeveloper%2FJayse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MelbourneDeveloper%2FJayse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MelbourneDeveloper%2FJayse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MelbourneDeveloper","download_url":"https://codeload.github.com/MelbourneDeveloper/Jayse/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243822312,"owners_count":20353496,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dart","flutter","immutable","json"],"created_at":"2024-10-11T01:25:48.727Z","updated_at":"2025-03-16T15:31:50.372Z","avatar_url":"https://github.com/MelbourneDeveloper.png","language":"Dart","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Jayse\n\n![Logo](https://github.com/MelbourneDeveloper/Jayse/raw/main/Images/IconSmall.png) \n\nLossless conversion of JSON to and from statically-typed, immutable objects in Dart.\n\n\u003csmall\u003e**Note**: this repo has two separate libraries (Dart/.NET) for working with JSON with the same name. They are currently different, but the aim for the long term is to bring them together and make them converge.\u003c/small\u003e\n\n[C# Package](src/dotnet)\n\n[Dart Package](src/dart_jayse)\n\n## Getting Started\n\nThere is no code generation, and there are no external dependencies. Just add the package to your `pubspec.yaml` file.\n\nYou can convert a JSON string to a `JsonObject` and access values like this:\n\n```dart\nimport 'package:jayse/jayse.dart';\n\nvoid main() {\n  final jo = jsonValueDecode('{\"name\": \"John Doe\", \"age\": 30}');\n\n  final name = jo['name'];\n  final age = jo['age'];\n\n  print('Name: $name, Age: $age');\n}\n```\n\nOutput: \n\u003e Name: 'John Doe', Age: 30\n\nBut, `name` and `age` are `JsonValue`s. They are strongly typed. There are several ways to access the value, but you can't just cast them to a `String` or `int` because this would involve casting (`as`). Jayse uses [`pattern matching`](https://dart.dev/language/patterns) under the hood and avoids casting where possible. \n\nThe accessors here return a subtype of `JsonValue`, which could be `JsonString`, `JsonNumber`, `JsonBoolean`, `JsonArray`, `JsonObject`, `JsonNull`, or `Undefined`. If you know what the value will be ahead of time, you can access the value like this. It will return `num` or null if the field doesn't exist, or the value is null.\n\n```dart\nnum? ageValue = age.numericValue;\n```\n\nYou can also handle values with a [switch expression](https://www.christianfindlay.com/blog/dart-switch-expressions). The important thing to understand about `JsonValue` is that there are only a preset number of subtypes. That means that you will get an analyzer warning/error if you don't handle all the cases in the switch expression. This is good because it forces you to handle all cases, and you won't get an exception from an unhandled case at runtime. This is good for handling cases where the data type might not be what you expect. For example:\n\n```dart\n    final ageValue2 = switch (age) {\n      (final JsonNumber jn) =\u003e jn.value,\n      (final JsonString js) =\u003e int.tryParse(js.value),\n      //TODO: other cases\n      //Catch all case...\n      _ =\u003e null,\n    };\n```\n\n## Features\n\n- **Powerful Path Parsing and Filtering** - allows you to access deeply nested values without casting and without throwing exceptions.\n- **Lossless Conversion**: convert to strongly typed Dart objects and back to JSON without any information loss. See below for more information.\n- **Strong Typing**: all values are strongly typed. No accessing `dynamic` values.\n- **Immutable**: All objects are immutable. There are no setters. Use non-destructive mutation to create new `JsonObject`s.\n- **Simple, tight code**: the library is small and easy to understand. It's a single file with no dependencies, and currently under 200 LOC.\n- **Simpler data classes and less code generation**: data classes are simpler and code generation with tools like `json_serializable` is often not necessary.\n\n```dart\nclass Message {\n  Message(this._jsonObject);\n\n  factory Message.fromJson(Map\u003cString, dynamic\u003e json) =\u003e\n      Message(JsonObject.fromJson(json));\n\n  final JsonObject _jsonObject;\n\n  bool? get isGood =\u003e _jsonObject.value('isGood');\n  String? get message =\u003e _jsonObject.value('message');\n\n  Map\u003cString, dynamic\u003e toJson() =\u003e _jsonObject.toJson();\n\n  Message copyWith({\n    bool? isGood,\n    String? message,\n  }) =\u003e\n      Message(\n        _jsonObject.withUpdates({\n          if (isGood != null) 'isGood': isGood.toJsonValue(),\n          if (message != null) 'message': message.toJsonValue(),\n        }),\n      );\n}\n```\n\n## JSON Path Parsing and Filtering\n\nJayse provides powerful capabilities for parsing and filtering JSON data using JSON paths. Easily navigate through complex JSON structures, extract specific values, and filter arrays based on custom criteria.\n\n### Parsing JSON Paths\n\nJayse allows you to parse JSON paths and retrieve the corresponding values from a JSON object. The `parseJsonPath` function takes a JSON path string and a `JsonValue` object as input and returns the value at the specified path.\n\n```dart\nfinal jsonValue = jsonValueDecode('''\n  {\n    \"name\": \"John Doe\",\n    \"age\": 30,\n    \"city\": \"New York\"\n  }\n''');\n\nfinal result = parseJsonPath(r'$.name', jsonValue);\nprint(result.stringValue); // Output: John Doe\n```\n\nJayse uses the simple and intuitive [JSONPath syntax](https://datatracker.ietf.org/doc/rfc9535/), documented as RFC 9535 to navigate through the JSON structure. Here are some examples:\n\n- `$`: Represents the root object.\n- `.`: Accesses a property of an object.\n- `[]`: Accesses an element of an array by its index.\n- `*`: Wildcard that matches all properties or elements.\n- `..`: Recursive descent to access all matching properties at any depth.\n\n```dart\nfinal jsonValue = jsonValueDecode('''\n  {\n    \"person\": {\n      \"name\": \"John\",\n      \"age\": 30,\n      \"address\": {\n        \"city\": \"New York\",\n        \"country\": \"USA\"\n      }\n    }\n  }\n''');\n\nfinal name = parseJsonPath(r'$.person.name', jsonValue).stringValue;\nprint(name); // Output: John\n\nfinal city = parseJsonPath(r'$.person.address.city', jsonValue).stringValue;\nprint(city); // Output: New York\n```\n\n## Path Extensions\n\nJayse provides convenient extension methods on the `JsonObject` class to simplify accessing values using JSON paths. These extensions allow you to retrieve values of specific types directly from a JSON object.\n\n```dart\nfinal jsonObject = jsonValueDecode('''\n  {\n    \"name\": \"John Doe\",\n    \"age\": 30,\n    \"isStudent\": false,\n    \"score\": 85.5,\n    \"graduation\": \"2022-06-30T10:00:00Z\"\n  }\n''') as JsonObject;\n\nfinal name = jsonObject.stringFromPath(r'$.name');\nprint(name); // Output: John Doe\n\nfinal age = jsonObject.integerFromPath(r'$.age');\nprint(age); // Output: 30\n\nfinal isStudent = jsonObject.boolFromPath(r'$.isStudent');\nprint(isStudent); // Output: false\n\nfinal score = jsonObject.doubleFromPath(r'$.score');\nprint(score); // Output: 85.5\n\nfinal graduation = jsonObject.dateTimeFromPath(r'$.graduation');\nprint(graduation); // Output: 2022-06-30 10:00:00.000\n```\n\nThese extensions provide a convenient way to extract values of specific types from a JSON object without the need for explicit casting.\n\n## Filtering Arrays with whereFromPath\n\nJayse offers a powerful `whereFromPath` extension method on `JsonValue` that allows you to filter arrays based on custom criteria. It takes a JSON path and a predicate function as input and returns a list of `JsonValue` objects that satisfy the predicate.\n\n```dart\nfinal jsonObject = jsonValueDecode('''\n  {\n    \"books\": [\n      {\n        \"title\": \"Book 1\",\n        \"author\": \"Author 1\",\n        \"price\": 10.99\n      },\n      {\n        \"title\": \"Book 2\",\n        \"author\": \"Author 2\",\n        \"price\": 15.99\n      },\n      {\n        \"title\": \"Book 3\",\n        \"author\": \"Author 1\",\n        \"price\": 12.99\n      }\n    ]\n  }\n''') as JsonObject;\n\nfinal expensiveBooks = jsonObject.whereFromPath(\n  r'$.books',\n  (book) =\u003e (book['price'].doubleValue ?? 0) \u003e 12,\n);\nprint(expensiveBooks); // Output: [Book 2]\n\nfinal authorBooks = jsonObject.whereFromPath(\n  r'$.books',\n  (book) =\u003e book['author'].stringValue == 'Author 1',\n);\nprint(authorBooks); // Output: [Book 1, Book 3]\n```\n\nThe `whereFromPath` method allows you to filter arrays based on any custom criteria. You can access nested properties, perform comparisons, and combine multiple conditions using logical operators.\n\nHere are a few more examples of using `whereFromPath` in real-life scenarios:\n\n1. Filtering products based on price range:\n```dart\nfinal products = jsonObject.whereFromPath(\n  r'$.products',\n  (product) {\n    final price = product['price'].doubleValue ?? 0;\n    return price \u003e= 50 \u0026\u0026 price \u003c= 100;\n  },\n);\n```\n\n1. Filtering users based on age and location:\n```dart\nfinal eligibleUsers = jsonObject.whereFromPath(\n  r'$.users',\n  (user) {\n    final age = user['age'].integerValue ?? 0;\n    final country = user['address']['country'].stringValue;\n    return age \u003e= 18 \u0026\u0026 country == 'USA';\n  },\n);\n```\n\n1. Filtering blog posts based on tags:\n```dart\nfinal taggedPosts = jsonObject.whereFromPath(\n  r'$.posts',\n  (post) =\u003e post['tags'].arrayValue?.contains(const JsonString('technology')) ?? false,\n);\n```\n\n1. Filtering movies based on ratings and genre:\n```dart\nfinal topActionMovies = jsonObject.whereFromPath(\n  r'$.movies',\n  (movie) {\n    final rating = movie['rating'].doubleValue ?? 0;\n    final genres = movie['genres'].arrayValue?.map((genre) =\u003e genre.stringValue);\n    return rating \u003e= 8.0 \u0026\u0026 genres?.contains('Action') ?? false;\n  },\n);\n```\n\nWhether you're working with APIs, processing large datasets, or manipulating JSON data in your Dart applications, Jayse simplifies the process and provides a clean and intuitive API for handling JSON paths and filtering.\n\nFor more details and advanced usage, please see the comprehensive [tests](https://github.com/MelbourneDeveloper/Jayse/blob/c1d3c9ef4aaa8f94d189c4eca61db82014c82d27/src/dart_jayse/test/parser_test.dart#L271).\n\n## What Problem Does It Solve?\n\nJayse attempts to solve the problem of data loss or corruption when serializing or deserializing JSON in Dart. Jayse facilitates safe and lossless conversion of JSON to and from statically-typed, immutable objects. When you receive data from a backend, you can modify it and send it back without destroying other data that arrived in the payload. This is in contrast with packages like `json_serializable` and `freezed`, which can corrupt data when converting JSON to Dart objects and back.\n\nSee the overall goal [here](../../README.md).\n\nLet's take a look at an example problem with the most common Dart package for dealing with JSON serialization `json_serializable`. The same problem occurs with all popular packages like `freezed` and so on. Here is a very simple scenario. The JSON payload has three fields: `name`, `age` and `gender`, but the `User` class is missing the `gender` field. Watch what happens to the JSON when we convert to `User` and back to JSON text.\n\nuser.dart\n```dart\nimport 'package:json_annotation/json_annotation.dart';\n\npart 'user.g.dart';\n\n@JsonSerializable()\nclass User {\n  final String name;\n  final int age;\n  @JsonKey(includeIfNull: false)\n  final String? email;\n\n  User({\n    required this.name,\n    required this.age,\n    this.email,\n  });\n\n  factory User.fromJson(Map\u003cString, dynamic\u003e json) =\u003e _$UserFromJson(json);\n  Map\u003cString, dynamic\u003e toJson() =\u003e _$UserToJson(this);\n}\n```\n\n```dart\nvoid main() {\n  // Original JSON data\n  final jsonString = '{\"name\": \"John Doe\", \"age\": 30, \"gender\": \"male\"}';\n\n  // Convert JSON to User object\n  final user = User.fromJson(json.decode(jsonString));\n  print('User object: $user');\n\n  // Convert User object back to JSON\n  final convertedJsonString = json.encode(user.toJson());\n  print(convertedJsonString);\n}\n```\n\nOriginal JSON:\n```json\n{\"name\": \"John Doe\", \"age\": 30, \"gender\": \"male\"}\n```\n\nOutput:\n```json\n{\"name\":\"John Doe\",\"age\":30}\n```\n\nNotice that the `gender` field was deleted.\n\nOk, so the `gender` field data was deleted, and that's to be expected because our data model is out of date on the Dart side. We might be able to tolerate that because our working assumption is that the Dart model will always be automatically generated and correct. But, what if we flip this around and convert JSON missing the `gender` field to `User` and back, where `User` has a `gender` field?\n\n```dart\n@JsonSerializable()\nclass User {\n  final String name;\n  final int age;\n  @JsonKey(includeIfNull: false)\n  final String? email;\n  final String? gender;\n\n  User({\n    required this.name,\n    required this.age,\n    this.email,\n    required this.gender,\n  });\n\n  factory User.fromJson(Map\u003cString, dynamic\u003e json) =\u003e _$UserFromJson(json);\n  Map\u003cString, dynamic\u003e toJson() =\u003e _$UserToJson(this);\n}\n```\n\n```dart\nvoid main() {\n  // Original JSON data\n  final jsonString = '{\"name\": \"John Doe\", \"age\": 30}';\n\n  // Convert JSON to User object\n  final user = User.fromJson(json.decode(jsonString));\n  print('User object: $user');\n\n  // Convert User object back to JSON\n  final convertedJsonString = json.encode(user.toJson());\n  print(convertedJsonString);\n}\n```\n\nOutput:\n\n```json\n{\"name\":\"John Doe\",\"age\":30,\"gender\":null}\n```\n\nNotice that the `gender` field was added with a `null` value. This is a problem because the original JSON did not have a `gender` field. This is corruption. If send this value back to the server, it may set an existing value to `null` even though the original value was not `null`.\n\n### Undefined\n\nWhile `Undefined` is not technically a JSON value, it does come up, and JavaScript handles this concept. JSON after all stands for JavaScript Object Notation, so ignoring undefined values is sticking your head in the sand. Jayse has a concept of `Undefined` which is a value that is not set. This is different from `null` which is a value that is set to `null`. \n\nMost importantly, Jayse distinguishes between undefined and null, so you can POST the object back to the server in the way that the server expects.\n\n### Field Order Preservation\n\nSome code is sensitive to the ordering of fields. For example, if you are using Firestore, the order of fields is important. If you convert JSON to a Dart object and back, the order of fields may change. This can cause problems with Firestore. Jayse preserves field ordering.\n\n### Extra Fields\n\nIf you receive an extra field in the payload that you don't know about, Jayse will preserve it. This is useful for forward compatibility. You can still send the data back to the server without losing the extra field.\n\n### Non-destructive Mutation\n\n`JsonObject`s have the `withUpdate` and other methods for cloning the object with updates. This allows you to create `copyWith` methods on your data classes. Calling these methods creates a new `JsonObject` with the updates applied, but does not mutate the original object.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmelbournedeveloper%2Fjayse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmelbournedeveloper%2Fjayse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmelbournedeveloper%2Fjayse/lists"}