Do not hesitate to look into the built-in ValueObjectVisitors, in
eZ/Publish/Core/REST/Server/Output/ValueObjectVisitor, for more examples.
The easiest way to handle cache is to re-use the
CachedValue Value Object. It acts as a proxy, and adds the cache headers, depending on the configuration, for a given object and set of options.
When you want the response to be cached, return an instance of CachedValue, with your Value Object as the argument. You can also pass a location id using the second argument, so that the Response is tagged with it:
return new CachedValue($helloValue, ['locationId', 42]);
What we have seen above covers requests that don't require an input payload, such as GET or DELETE. If you need to provide your controller with parameters, either in JSON or XML, the parameter struct requires an Input Parser so that the payload can be converted to an actual ValueObject.