https://github.com/MartinGeisse/grumpyrest Skip to content Toggle navigation Sign up * Product + Actions Automate any workflow + Packages Host and manage packages + Security Find and fix vulnerabilities + Codespaces Instant dev environments + Copilot Write better code with AI + Code review Manage code changes + Issues Plan and track work + Discussions Collaborate outside of code Explore + All features + Documentation + GitHub Skills + Blog * Solutions For + Enterprise + Teams + Startups + Education By Solution + CI/CD & Automation + DevOps + DevSecOps Case Studies + Customer Stories + Resources * Open Source + GitHub Sponsors Fund open source developers + The ReadME Project GitHub community articles Repositories + Topics + Trending + Collections * Pricing [ ] * # In this repository All GitHub | Jump to | * No suggested jump to results * # In this repository All GitHub | Jump to | * # In this user All GitHub | Jump to | * # In this repository All GitHub | Jump to | Sign in Sign up You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session. {{ message }} MartinGeisse / grumpyrest Public * Notifications * Fork 0 * Star 36 License MIT license 36 stars 0 forks Star Notifications * Code * Issues 0 * Pull requests 0 * Actions * Projects 0 * Security * Insights More * Code * Issues * Pull requests * Actions * Projects * Security * Insights MartinGeisse/grumpyrest This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository. master Switch branches/tags [ ] Branches Tags Could not load branches Nothing to show {{ refName }} default View all branches Could not load tags Nothing to show {{ refName }} default View all tags Name already in use A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch? Cancel Create 1 branch 0 tags Code * Local * Codespaces * Clone HTTPS GitHub CLI [https://github.com/M] Use Git or checkout with SVN using the web URL. [gh repo clone Martin] Work fast with our official CLI. Learn more about the CLI. * Open with GitHub Desktop * Download ZIP Sign In Required Please sign in to use Codespaces. Launching GitHub Desktop If nothing happens, download GitHub Desktop and try again. Launching GitHub Desktop If nothing happens, download GitHub Desktop and try again. Launching Xcode If nothing happens, download Xcode and try again. Launching Visual Studio Code Your codespace will open once ready. There was a problem preparing your codespace, please try again. Latest commit @MartinGeisse MartinGeisse to-do list ... 91862e1 Jun 11, 2023 to-do list 91862e1 Git stats * 103 commits Files Permalink Failed to load latest commit information. Type Name Latest commit message Commit time doc gradle/wrapper src .gitignore LICENSE README.md build.gradle gradlew gradlew.bat View code grumpyrest How does this look in code? What about performance? JSON Mapping Nullable and Optional Properties Validation README.md grumpyrest grumpyrest is a Java REST server framework that does not use annotations, automatic dependency injection or reactive streams, and minimizes the use of reflection. Instead, * it leverages the Java type system to indicate the meaning of classes, fields and methods * it calls constructors to create dependency objects, and passes constructor parameters to inject them * it uses threads to achieve parallelism, and in particular virtual threads for highly parallel I/O Particular differences with typical REST frameworks: * To mount an endpoint to a URL, you do not annotate the handler method. Instead, you call a mount method and pass the handler as a parameter. * To access request properties such as URL path parameters, querystring parameters or the request body, you do not declare handler parameters. Instead, you call a method. This method takes a specification of the expected type and performs validation before returning its data. * Request/response bodies are typically expected to be JSON. To declare how the request body is parsed from JSON, as well as validations to perform, you use appropriate Java types that validate their parameters / fields in their constructor. Similarly, to declare how the response objects are mapped to JSON, you use appropriate Java types with a well-defined JSON mapping. Mappings in both directions can be registered from outside the data-holding classes to support existing classes. * This includes nullable and optional fields: Appropriate generic wrapper types are provided to define nullable and/or optional fields. Java's null references are NOT considered equivalent to JSON-null nor to absent JSON properties, and having null references on the Java side of JSON mapping is considered an error to avoid any ambiguities. * No type / field is nullable or optional by default. No default values will be inserted for missing fields. No coercion from number to string or from string to number will be done by default. An empty JSON object, empty JSON array, false, 0, null and missing fields are all different things that are not considered equal. If you want to bend these rules for specific types or fields, use an appropriate type for that which defines a relaxed mapping. If you want to bend these rules for all of your JSON mapping, replace the built-in type adapters by your own which define a relaxed mapping. How does this look in code? Here's a simple API that builds a greeting: RestApi api = new RestApi(); api.addRoute("/make-greeting", requestCycle -> { MakeGreetingRequest request = requestCycle.parseBody(MakeGreetingRequest.class); if (request.addendum.isPresent()) { return new MakeGreetingResponse("Hello, " + request.name + "! " + request.addendum.getValue()); } else { return new MakeGreetingResponse("Hello, " + request.name + "!"); } }); Request 1: {"name": "Joe"} Response 1: {"greeting": "Hello, Joe!"} Request 2: {"name": "Joe", "addendum": "Nice to meet you."} Response 2: {"greeting": "Hello, Joe! Nice to meet you."} What about performance? It is anticipated that grumpyrest may provide tools to tackle potential performance problems, which rely on reactive programming and/or code generation. The goal of grumpyrest is not to totally avoid these things internally, but to allow the developers using it to totally avoid them in their application code. This means: * We will not add anything like that to grumpyrest until it is perfectly clear that it solves a performance problem in modern Java which, for example, virtual threads cannot handle. * None of this affects application code, unless you choose to use it in application code (because the performance problem lies there instead of inside grumpyrest). * Any such features can be turned of to exclude them as a potential source of bugs, as well as to single-step debug into grumpyrest to understand what happens inside (e.g. to track down a specific bug). We all know what single-step debugging into highly asynchronous / reactive or generated code is like. JSON Mapping A mapping exists between Java records and JSON objects: record CategoryLink(int id, String name) {} record ProductResponse(CategoryLink category, String name, String description, int unitPrice) {} new ProductResponse(new CategoryLink(2, "Widgets"), "Left-handed Hammer", "Excellent choice for left-handed people!", 10) <-> { "category": { "id": 2, "name": "Widgets" }, "name": "Left-handed Hammer", "description": "Excellent choice for left-handed people!", "unitPrice": 10 } A mapping for non-record objects could be defined, but the strong guarantees about their shape that come from the language make the mapping very easy. Also, I just like that they are immutable. JSON arrays can be mapped to Guava's ImmutableList: ImmutableList.of(1, 2, 3) <-> [1, 2, 3] Again, normal List / ArrayList could be mapped too, but I just like ImmutableList. JSON primitives are mapped to Java's built-in types, both boxed and unboxed. No coercion takes place, so for example the number 123 cannot be parsed into a String-typed field (unless you override the type adapter for type String to accept numbers). Nullable and Optional Properties Object properties are neither nullable nor optional by default. That is, a missing property in JSON causes an error, and so does a property that is null in JSON. The Java record can use the type NullableField to allow null in JSON, but on the Java side, it is still not null but a NullableField without a value. This works similar to java.util.Optional but IDEs complain if you actually use java.util.Optional except in a few specific places, and besides that nullable properties and optional properties are different things, So I defined my own types. Similar to nullable fields, the type OptionalField defines a property which may be absent from its record. Again, I didn't use java.util.Optional because that type doesn't seem to be meant for using it that way. This type can only be used in records since a top-level value cannot be just absent, and for JSON array elements, an "optional" property can never be parsed as absent and acts as a poor man's filter during serialization. So there is no added value in allowing this in JSON arrays. Like NullableField, a missing optional property corresponds to an OptionalField object without a value on the Java side, not to a field that is directly null in the containing record. As you may have guessed by now, Java's null reference never appears in JSON mapping: The parser won't produce it and the serializer will throw an exception if you give it a record with a field that is null. Java's null references are just too easy to get wrong, and especially, are ambiguous with respect to the fact that a property in JSON may be nullable or optional. It may even be both: An OptionalField on the Java side maps to a JSON field that may be absent, or null, or have a value. Unknown properties in records cause an error too. A type adapter which collects extra properties in a Map<> may be defined in the future. Validation Other JSON mapping frameworks either define how they do validation after parsing, or say that validation is not their business. In contrast, I do not think that it is possible to split parsing and validation into two different phases in any meaningful way. To split them, there would have to be a Java type that results from parsing that represents the data in a parsed but unvalidated state, which is then passed to validation. This implies one of three possible things: * either that such data is defined as Java classes which routinely do not validate their state in their constructor, so they are happy with being in an inconsistent state. It might be even worse and such classes might not just delay validation of their state until later, but they might even rely on external validation rules to do so, i.e. they do not have an independent notion of what it means for their state to be "valid". * or the classes used for JSON mapping are built outright as a set of dumb data containers, with the sole purpose of making the JSON data accessible from Java code, and getting away from those classes agin as fast as possible. This adds unnecessary complexity (there is a whole extra layer in your code without any real purpose) and validation is actually harder now, because you have to define rules for your JSON but express them as rules in Java. This is one of the reasons we have OptionalField and NullableField as two separate classes, because good luck if you have a null reference in Java and when building an error message you want to know if the client sent null or forgot that field. * or you aren't even defining rigorous validation rules, and impose whatever meaning is closest on the incoming JSON data. Code that does this will happily ignore extra fields in records, coerce numbers to strings, give missing properties and null properties the same meaning, and so on. This does not mean that your code is unsafe -- safety is implemented by the code that does something with that data, and is rarely dependent on JSON validation. However, I think that being careless about validation will steer you directly into hell when it comes to versioning you JSON API payloads, and making changes to them, because the clients calling your API are almost guaranteed to be dependent on behavior of your API in cases that you never defined. The bottom line is that we don't have any validation after parsing. Rather, validation is part of parsing. If signing up to your service requires users to be at least 18 years old, validate (age >= 18) in the constructor of your SignUpRequestBody. If the username cannot be empty, check that too in the same constructor. Alternatively, if usernames must be specific in many places and must be validated to be nonempty (and possibly at least 5 characters, and not contain ASCII control characters nor offensive words), define a UserName type that checks these things in its constructor. If you have an existing typpe chose constructor cannot be changed, wrap it in a type that defines its validity rules. Or, as the last resort, you can define a custom type adapter and register it in the JsonRegistry that defines how a type gets parsed and serialized. But there is no validation after parsing, period. About No description, website, or topics provided. Resources Readme License MIT license Stars 36 stars Watchers 2 watching Forks 0 forks Report repository Releases No releases published Packages 0 No packages published Languages * Java 100.0% Footer (c) 2023 GitHub, Inc. Footer navigation * Terms * Privacy * Security * Status * Docs * Contact GitHub * Pricing * API * Training * Blog * About You can't perform that action at this time.