Repository: jooby-project/jooby (java)

Issue:
ModelAndView that is not a Map
I am using the [jte.gg](https://jte.gg) templating engine and it allows your model to be a class, so if I have a class called Model and want to pass this to ModelAndView I cannot do so because ModelAndView only takes a Map<String, Object>.

~~I tried to pass a `Map.of("model", model)` but the JTE file does not recognize this, the `@param` value becomes null. I am using precompiled templates with JTE.~~ EDIT: That was due to another bug which I have submitted a fix for now, #3300.

Pull request:
template engine: ModelAndView that is not a Map
- Allow to use anything as Model
- Remove `put()` methods from ModelAndView
- Introduce `MapModelAndView`
- Replace `new ModelAndView(String, Map)` by `ModelAndView.map()`
- fix #3113

Patch:
diff --git a/docs/asciidoc/templates.adoc b/docs/asciidoc/templates.adoc
index d1e790c26..3702d6245 100644
--- a/docs/asciidoc/templates.adoc
+++ b/docs/asciidoc/templates.adoc
@@ -9,7 +9,7 @@ Templates are available via javadoc:ModelAndView[] and requires a javadoc:Templa
   install(new MyTemplateEngineModule());            <1>
   
   get("/", ctx -> {
-    Map<String, Object> model = ...;                <2>
+    MyModel model = ...;                <2>
     return new ModelAndView("index.html", model);  <3>
   });
 }
@@ -22,7 +22,7 @@ Templates are available via javadoc:ModelAndView[] and requires a javadoc:Templa
   install(MyTemplateEngineModule())                 <1>
   
   get("/") { ctx ->
-    val model = mapOf(...)                          <2>
+    val model = MyModel(...)                          <2>
     ModelAndView("index.html", model)              <3>
   }
 }
@@ -41,7 +41,7 @@ javadoc:ModelAndView[] allows you to specify the desired locale used for templat
   install(new MyTemplateEngineModule());
 
   get("/", ctx -> {
-    Map<String, Object> model = ...;
+    MyModel model = ...;
     return new ModelAndView("index.html", model)
         .setLocale(Locale.GERMAN);                <1>
   });
@@ -55,7 +55,7 @@ javadoc:ModelAndView[] allows you to specify the desired locale used for templat
   install(MyTemplateEngineModule())
 
   get("/") { ctx ->
-    val model = mapOf(...)
+    val model = MyModel(...)
     ModelAndView("index.html", model)
         .setLocale(Locale.GERMAN)                <1>
   }
@@ -127,3 +127,12 @@ The file extension allow us to use/mix multiple template engines too:
 <4> Render using Freemarker, `.ftl` extension
 
 Checkout all the available <<modules-template-engine, template engines>> provided by Jooby.
+
+=== View Model
+
+Since Jooby `3.1.x` the model can be anything object you like, previous version requires to be always `map`. There
+are two implementations of `ModelAndView`:
+
+- ModelAndView(String view, Object model)
+- MapModelAndView(String view, Map<String, Object> model)
+
diff --git a/jooby/src/main/java/io/jooby/MapModelAndView.java b/jooby/src/main/java/io/jooby/MapModelAndView.java
new file mode 100644
index 000000000..ed46df631
--- /dev/null
+++ b/jooby/src/main/java/io/jooby/MapModelAndView.java
@@ -0,0 +1,63 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby;
+
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import edu.umd.cs.findbugs.annotations.Nullable;
+
+public class MapModelAndView extends ModelAndView<Map<String, Object>> {
+  /**
+   * Creates a new model and view.
+   *
+   * @param view View name must include file extension.
+   * @param model View model.
+   */
+  public MapModelAndView(@NonNull String view, @NonNull Map<String, Object> model) {
+    super(view, model);
+  }
+
+  /**
+   * Creates a new model and view.
+   *
+   * @param view View name must include file extension.
+   */
+  public MapModelAndView(@NonNull String view) {
+    super(view, new LinkedHashMap<>());
+  }
+
+  /**
+   * Put a model attribute.
+   *
+   * @param name Name.
+   * @param value Value.
+   * @return This model and view.
+   */
+  public MapModelAndView put(@NonNull String name, Object value) {
+    model.put(name, value);
+    return this;
+  }
+
+  /**
+   * Copy all the attributes into the model.
+   *
+   * @param attributes Attributes.
+   * @return This model and view.
+   */
+  public MapModelAndView put(@NonNull Map<String, Object> attributes) {
+    this.model.putAll(attributes);
+    return this;
+  }
+
+  @Override
+  public MapModelAndView setLocale(@Nullable Locale locale) {
+    super.setLocale(locale);
+    return this;
+  }
+}
diff --git a/jooby/src/main/java/io/jooby/ModelAndView.java b/jooby/src/main/java/io/jooby/ModelAndView.java
index 59b91ff34..e15187741 100644
--- a/jooby/src/main/java/io/jooby/ModelAndView.java
+++ b/jooby/src/main/java/io/jooby/ModelAndView.java
@@ -5,7 +5,6 @@
  */
 package io.jooby;
 
-import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
 
@@ -18,13 +17,13 @@ import edu.umd.cs.findbugs.annotations.Nullable;
  * @since 2.0.0
  * @author edgar
  */
-public class ModelAndView {
+public class ModelAndView<T> {
 
   /** View name. */
   private final String view;
 
   /** View data. */
-  private final Map<String, Object> model;
+  protected final T model;
 
   /** Locale used when rendering the view. */
   private Locale locale;
@@ -35,41 +34,30 @@ public class ModelAndView {
    * @param view View name must include file extension.
    * @param model View model.
    */
-  public ModelAndView(@NonNull String view, @NonNull Map<String, Object> model) {
+  public ModelAndView(@NonNull String view, @NonNull T model) {
     this.view = view;
     this.model = model;
   }
 
   /**
-   * Creates a new model and view.
+   * Creates a model and view backed by a map.
    *
-   * @param view View name must include file extension.
+   * @param view View name.
+   * @return A map model and view.
    */
-  public ModelAndView(@NonNull String view) {
-    this(view, new HashMap<>());
+  public static MapModelAndView map(@NonNull String view) {
+    return new MapModelAndView(view);
   }
 
   /**
-   * Put a model attribute.
+   * Creates a model and view backed by a map.
    *
-   * @param name Name.
-   * @param value Value.
-   * @return This model and view.
+   * @param view View name.
+   * @param model Map instance.
+   * @return A map model and view.
    */
-  public ModelAndView put(@NonNull String name, Object value) {
-    model.put(name, value);
-    return this;
-  }
-
-  /**
-   * Copy all the attributes into the model.
-   *
-   * @param attributes Attributes.
-   * @return This model and view.
-   */
-  public ModelAndView put(@NonNull Map<String, Object> attributes) {
-    model.putAll(attributes);
-    return this;
+  public static MapModelAndView map(@NonNull String view, @NonNull Map<String, Object> model) {
+    return new MapModelAndView(view, model);
   }
 
   /**
@@ -79,7 +67,7 @@ public class ModelAndView {
    * @param locale The locale used when rendering the view.
    * @return This instance.
    */
-  public ModelAndView setLocale(@Nullable Locale locale) {
+  public ModelAndView<T> setLocale(@Nullable Locale locale) {
     this.locale = locale;
     return this;
   }
@@ -89,7 +77,7 @@ public class ModelAndView {
    *
    * @return View data (a.k.a as model).
    */
-  public Map<String, Object> getModel() {
+  public T getModel() {
     return model;
   }
 
diff --git a/modules/jooby-freemarker/src/main/java/io/jooby/freemarker/FreemarkerTemplateEngine.java b/modules/jooby-freemarker/src/main/java/io/jooby/freemarker/FreemarkerTemplateEngine.java
index 8c314642a..38e7d30ec 100644
--- a/modules/jooby-freemarker/src/main/java/io/jooby/freemarker/FreemarkerTemplateEngine.java
+++ b/modules/jooby-freemarker/src/main/java/io/jooby/freemarker/FreemarkerTemplateEngine.java
@@ -7,17 +7,14 @@ package io.jooby.freemarker;
 
 import java.io.StringWriter;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 import freemarker.core.Environment;
-import freemarker.template.Configuration;
-import freemarker.template.Template;
+import freemarker.template.*;
 import io.jooby.Context;
 import io.jooby.ModelAndView;
+import io.jooby.SneakyThrows;
 import io.jooby.TemplateEngine;
 
 class FreemarkerTemplateEngine implements TemplateEngine {
@@ -37,16 +34,21 @@ class FreemarkerTemplateEngine implements TemplateEngine {
 
   @Override
   public String render(Context ctx, ModelAndView modelAndView) throws Exception {
-    Template template = freemarker.getTemplate(modelAndView.getView());
-    StringWriter writer = new StringWriter();
-    Map<String, Object> model = new HashMap<>(ctx.getAttributes());
-    model.putAll(modelAndView.getModel());
-    Locale locale = modelAndView.getLocale();
+    var template = freemarker.getTemplate(modelAndView.getView());
+    var writer = new StringWriter();
+    var wrapper = freemarker.getObjectWrapper();
+    var model = modelAndView.getModel();
+    var engineModel = wrapper.wrap(model);
+    var locale = modelAndView.getLocale();
     if (locale == null) {
       locale = ctx.locale();
     }
-    Environment env = template.createProcessingEnvironment(model, writer);
+    Environment env = template.createProcessingEnvironment(engineModel, writer);
     env.setLocale(locale);
+    ctx.getAttributes()
+        .forEach(
+            SneakyThrows.throwingConsumer(
+                (name, value) -> env.setVariable(name, wrapper.wrap(value))));
     env.process();
     return writer.toString();
   }
diff --git a/modules/jooby-handlebars/src/main/java/io/jooby/internal/handlebars/HandlebarsTemplateEngine.java b/modules/jooby-handlebars/src/main/java/io/jooby/internal/handlebars/HandlebarsTemplateEngine.java
index 13f710858..f5acb1974 100644
--- a/modules/jooby-handlebars/src/main/java/io/jooby/internal/handlebars/HandlebarsTemplateEngine.java
+++ b/modules/jooby-handlebars/src/main/java/io/jooby/internal/handlebars/HandlebarsTemplateEngine.java
@@ -5,10 +5,10 @@
  */
 package io.jooby.internal.handlebars;
 
+import static com.github.jknack.handlebars.Context.newContext;
+
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 import com.github.jknack.handlebars.Handlebars;
 import com.github.jknack.handlebars.Template;
@@ -35,8 +35,7 @@ public class HandlebarsTemplateEngine implements TemplateEngine {
   @Override
   public String render(Context ctx, ModelAndView modelAndView) throws Exception {
     Template template = handlebars.compile(modelAndView.getView());
-    Map<String, Object> model = new HashMap<>(ctx.getAttributes());
-    model.putAll(modelAndView.getModel());
-    return template.apply(model);
+    var engineModel = newContext(modelAndView.getModel()).data(ctx.getAttributes());
+    return template.apply(engineModel);
   }
 }
diff --git a/modules/jooby-jte/src/main/java/io/jooby/jte/JteTemplateEngine.java b/modules/jooby-jte/src/main/java/io/jooby/jte/JteTemplateEngine.java
index 1247187f4..acf73fd35 100644
--- a/modules/jooby-jte/src/main/java/io/jooby/jte/JteTemplateEngine.java
+++ b/modules/jooby-jte/src/main/java/io/jooby/jte/JteTemplateEngine.java
@@ -7,12 +7,12 @@ package io.jooby.jte;
 
 import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 import gg.jte.TemplateEngine;
 import gg.jte.output.StringOutput;
 import io.jooby.Context;
+import io.jooby.MapModelAndView;
 import io.jooby.ModelAndView;
 
 class JteTemplateEngine implements io.jooby.TemplateEngine {
@@ -33,15 +33,15 @@ class JteTemplateEngine implements io.jooby.TemplateEngine {
   public String render(Context ctx, ModelAndView modelAndView) {
     var output = new StringOutput();
     var attributes = ctx.getAttributes();
-    Map<String, Object> model;
-    if (attributes.isEmpty()) {
-      model = modelAndView.getModel();
+    if (modelAndView instanceof MapModelAndView mapModelAndView) {
+      var mapModel = new HashMap<>();
+      mapModel.putAll(attributes);
+      mapModel.putAll(mapModelAndView.getModel());
+      jte.render(modelAndView.getView(), mapModel, output);
     } else {
-      model = new HashMap<>();
-      model.putAll(attributes);
-      model.putAll(modelAndView.getModel());
+      jte.render(modelAndView.getView(), modelAndView.getModel(), output);
     }
-    jte.render(modelAndView.getView(), model, output);
+
     return output.toString();
   }
 }
diff --git a/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java b/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java
index 8feaee3a3..f89d4eeb3 100644
--- a/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java
+++ b/modules/jooby-pebble/src/main/java/io/jooby/pebble/PebbleTemplateEngine.java
@@ -15,6 +15,7 @@ import java.util.Map;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
 import io.jooby.Context;
+import io.jooby.MapModelAndView;
 import io.jooby.ModelAndView;
 import io.jooby.TemplateEngine;
 import io.pebbletemplates.pebble.PebbleEngine;
@@ -35,17 +36,27 @@ class PebbleTemplateEngine implements TemplateEngine {
     return extensions;
   }
 
+  @Override
+  public boolean supports(@NonNull ModelAndView modelAndView) {
+    return TemplateEngine.super.supports(modelAndView) && modelAndView instanceof MapModelAndView;
+  }
+
   @Override
   public String render(Context ctx, ModelAndView modelAndView) throws Exception {
-    PebbleTemplate template = engine.getTemplate(modelAndView.getView());
-    Writer writer = new StringWriter();
-    Map<String, Object> model = new HashMap<>(ctx.getAttributes());
-    model.putAll(modelAndView.getModel());
-    Locale locale = modelAndView.getLocale();
-    if (locale == null) {
-      locale = ctx.locale();
+    if (modelAndView instanceof MapModelAndView mapModelAndView) {
+      PebbleTemplate template = engine.getTemplate(modelAndView.getView());
+      Writer writer = new StringWriter();
+      Map<String, Object> model = new HashMap<>(ctx.getAttributes());
+      model.putAll(mapModelAndView.getModel());
+      Locale locale = modelAndView.getLocale();
+      if (locale == null) {
+        locale = ctx.locale();
+      }
+      template.evaluate(writer, model, locale);
+      return writer.toString();
+    } else {
+      throw new IllegalArgumentException(
+          "Only " + MapModelAndView.class.getName() + " are supported");
     }
-    template.evaluate(writer, model, locale);
-    return writer.toString();
   }
 }
diff --git a/modules/jooby-thymeleaf/src/main/java/io/jooby/internal/thymeleaf/ThymeleafTemplateEngine.java b/modules/jooby-thymeleaf/src/main/java/io/jooby/internal/thymeleaf/ThymeleafTemplateEngine.java
index 539cda061..1072f0a30 100644
--- a/modules/jooby-thymeleaf/src/main/java/io/jooby/internal/thymeleaf/ThymeleafTemplateEngine.java
+++ b/modules/jooby-thymeleaf/src/main/java/io/jooby/internal/thymeleaf/ThymeleafTemplateEngine.java
@@ -15,6 +15,7 @@ import org.thymeleaf.TemplateEngine;
 import org.thymeleaf.context.Context;
 
 import edu.umd.cs.findbugs.annotations.NonNull;
+import io.jooby.MapModelAndView;
 import io.jooby.ModelAndView;
 
 public class ThymeleafTemplateEngine implements io.jooby.TemplateEngine {
@@ -32,22 +33,33 @@ public class ThymeleafTemplateEngine implements io.jooby.TemplateEngine {
     return extensions;
   }
 
+  @Override
+  public boolean supports(@NonNull ModelAndView modelAndView) {
+    return io.jooby.TemplateEngine.super.supports(modelAndView)
+        && modelAndView instanceof MapModelAndView;
+  }
+
   @Override
   public String render(io.jooby.Context ctx, ModelAndView modelAndView) {
-    Map<String, Object> model = new HashMap<>(ctx.getAttributes());
-    model.putAll(modelAndView.getModel());
+    if (modelAndView instanceof MapModelAndView mapModelAndView) {
+      Map<String, Object> model = new HashMap<>(ctx.getAttributes());
+      model.putAll(mapModelAndView.getModel());
 
-    // Locale:
-    Locale locale = modelAndView.getLocale();
-    if (locale == null) {
-      locale = ctx.locale();
-    }
+      // Locale:
+      Locale locale = modelAndView.getLocale();
+      if (locale == null) {
+        locale = ctx.locale();
+      }
 
-    Context context = new Context(locale, model);
-    String templateName = modelAndView.getView();
-    if (!templateName.startsWith("/")) {
-      templateName = "/" + templateName;
+      Context context = new Context(locale, model);
+      String templateName = modelAndView.getView();
+      if (!templateName.startsWith("/")) {
+        templateName = "/" + templateName;
+      }
+      return templateEngine.process(templateName, context);
+    } else {
+      throw new IllegalArgumentException(
+          "Only " + MapModelAndView.class.getName() + " are supported");
     }
-    return templateEngine.process(templateName, context);
   }
 }


Test patch:
diff --git a/modules/jooby-freemarker/src/test/java/io/jooby/freemarker/FreemarkerModuleTest.java b/modules/jooby-freemarker/src/test/java/io/jooby/freemarker/FreemarkerModuleTest.java
index 1871f4794..7e012db95 100644
--- a/modules/jooby-freemarker/src/test/java/io/jooby/freemarker/FreemarkerModuleTest.java
+++ b/modules/jooby-freemarker/src/test/java/io/jooby/freemarker/FreemarkerModuleTest.java
@@ -14,6 +14,7 @@ import java.time.ZoneId;
 import java.time.temporal.TemporalAdjusters;
 import java.util.Arrays;
 import java.util.Date;
+import java.util.List;
 import java.util.Locale;
 
 import org.junit.jupiter.api.Test;
@@ -71,7 +72,7 @@ public class FreemarkerModuleTest {
     String output =
         engine.render(
             ctx,
-            new ModelAndView("index.ftl").put("user", new User("foo", "bar")).put("sign", "!"));
+            ModelAndView.map("index.ftl").put("user", new User("foo", "bar")).put("sign", "!"));
     assertEquals("Hello foo bar var!", output.trim());
   }
 
@@ -96,7 +97,7 @@ public class FreemarkerModuleTest {
     assertEquals(
         "friday",
         engine
-            .render(ctx, new ModelAndView("locales.ftl").put("someDate", nextFriday))
+            .render(ctx, ModelAndView.map("locales.ftl").put("someDate", nextFriday))
             .trim()
             .toLowerCase());
 
@@ -105,7 +106,7 @@ public class FreemarkerModuleTest {
         engine
             .render(
                 ctx,
-                new ModelAndView("locales.ftl")
+                ModelAndView.map("locales.ftl")
                     .put("someDate", nextFriday)
                     .setLocale(new Locale("en", "GB")))
             .trim()
@@ -116,7 +117,7 @@ public class FreemarkerModuleTest {
         engine
             .render(
                 ctx,
-                new ModelAndView("locales.ftl")
+                ModelAndView.map("locales.ftl")
                     .put("someDate", nextFriday)
                     .setLocale(Locale.GERMAN))
             .trim()
@@ -136,7 +137,7 @@ public class FreemarkerModuleTest {
     String output =
         engine.render(
             ctx,
-            new ModelAndView("index.ftl").put("user", new MyModel("foo", "bar")).put("sign", "!"));
+            ModelAndView.map("index.ftl").put("user", new MyModel("foo", "bar")).put("sign", "!"));
     assertEquals("Hello foo bar var!", output.trim());
   }
 
@@ -149,12 +150,11 @@ public class FreemarkerModuleTest {
                     getClass().getClassLoader(),
                     ConfigFactory.empty()
                         .withValue("templates.path", ConfigValueFactory.fromAnyRef("foo"))));
-    FreemarkerTemplateEngine engine =
-        new FreemarkerTemplateEngine(freemarker, Arrays.asList(".ftl"));
+    FreemarkerTemplateEngine engine = new FreemarkerTemplateEngine(freemarker, List.of(".ftl"));
     MockContext ctx =
         new MockContext().setRouter(new Jooby().setLocales(singletonList(Locale.ENGLISH)));
     ctx.getAttributes().put("local", "var");
-    String output = engine.render(ctx, new ModelAndView("index.ftl"));
+    String output = engine.render(ctx, ModelAndView.map("index.ftl"));
     assertEquals("var", output.trim());
   }
 }
diff --git a/modules/jooby-handlebars/src/test/java/io/jooby/handlebars/HandlebarsModuleTest.java b/modules/jooby-handlebars/src/test/java/io/jooby/handlebars/HandlebarsModuleTest.java
index 84d6c71fa..7c45b4f8b 100644
--- a/modules/jooby-handlebars/src/test/java/io/jooby/handlebars/HandlebarsModuleTest.java
+++ b/modules/jooby-handlebars/src/test/java/io/jooby/handlebars/HandlebarsModuleTest.java
@@ -51,7 +51,7 @@ public class HandlebarsModuleTest {
     String output =
         engine.render(
             ctx,
-            new ModelAndView("index.hbs").put("user", new User("foo", "bar")).put("sign", "!"));
+            ModelAndView.map("index.hbs").put("user", new User("foo", "bar")).put("sign", "!"));
     assertEquals("Hello foo bar var!", output.trim());
   }
 
@@ -68,7 +68,7 @@ public class HandlebarsModuleTest {
     String output =
         engine.render(
             ctx,
-            new ModelAndView("index.hbs").put("user", new User("foo", "bar")).put("sign", "!"));
+            ModelAndView.map("index.hbs").put("user", new User("foo", "bar")).put("sign", "!"));
     assertEquals("Hello foo bar var!", output.trim());
   }
 }
diff --git a/modules/jooby-pebble/src/test/java/io/jooby/pebble/PebbleModuleTest.java b/modules/jooby-pebble/src/test/java/io/jooby/pebble/PebbleModuleTest.java
index f5e6e9d17..3e5d0d8dc 100644
--- a/modules/jooby-pebble/src/test/java/io/jooby/pebble/PebbleModuleTest.java
+++ b/modules/jooby-pebble/src/test/java/io/jooby/pebble/PebbleModuleTest.java
@@ -54,7 +54,7 @@ public class PebbleModuleTest {
     String output =
         engine.render(
             ctx,
-            new ModelAndView("index.peb").put("user", new User("foo", "bar")).put("sign", "!"));
+            ModelAndView.map("index.peb").put("user", new User("foo", "bar")).put("sign", "!"));
     assertEquals("Hello foo bar var!", output);
   }
 
@@ -72,7 +72,7 @@ public class PebbleModuleTest {
     String output =
         engine.render(
             ctx,
-            new ModelAndView("index.peb").put("user", new User("foo", "bar")).put("sign", "!"));
+            ModelAndView.map("index.peb").put("user", new User("foo", "bar")).put("sign", "!"));
     assertEquals("Hello foo bar var!", output);
   }
 
@@ -86,21 +86,21 @@ public class PebbleModuleTest {
     MockContext ctx =
         new MockContext().setRouter(new Jooby().setLocales(singletonList(Locale.ENGLISH)));
 
-    assertEquals("Greetings!", engine.render(ctx, new ModelAndView("locales.peb")));
+    assertEquals("Greetings!", engine.render(ctx, ModelAndView.map("locales.peb")));
 
     assertEquals(
         "Hi!",
-        engine.render(ctx, new ModelAndView("locales.peb").setLocale(new Locale("en", "GB"))));
+        engine.render(ctx, ModelAndView.map("locales.peb").setLocale(new Locale("en", "GB"))));
 
     assertEquals(
-        "Grüß Gott!", engine.render(ctx, new ModelAndView("locales.peb").setLocale(Locale.GERMAN)));
+        "Grüß Gott!", engine.render(ctx, ModelAndView.map("locales.peb").setLocale(Locale.GERMAN)));
 
     assertEquals(
         "Grüß Gott!",
-        engine.render(ctx, new ModelAndView("locales.peb").setLocale(Locale.GERMANY)));
+        engine.render(ctx, ModelAndView.map("locales.peb").setLocale(Locale.GERMANY)));
 
     assertEquals(
         "Servus!",
-        engine.render(ctx, new ModelAndView("locales.peb").setLocale(new Locale("de", "AT"))));
+        engine.render(ctx, ModelAndView.map("locales.peb").setLocale(new Locale("de", "AT"))));
   }
 }
diff --git a/tests/src/test/java/io/jooby/test/FeaturedTest.java b/tests/src/test/java/io/jooby/test/FeaturedTest.java
index 6b7281c33..7b3648bef 100644
--- a/tests/src/test/java/io/jooby/test/FeaturedTest.java
+++ b/tests/src/test/java/io/jooby/test/FeaturedTest.java
@@ -3589,7 +3589,7 @@ public class FeaturedTest {
         .define(
             app -> {
               app.install(new HandlebarsModule());
-              app.get("/home", ctx -> new ModelAndView("flash.hbs"));
+              app.get("/home", ctx -> ModelAndView.map("flash.hbs"));
             })
         .dontFollowRedirects()
         .ready(
@@ -4444,7 +4444,7 @@ public class FeaturedTest {
 
               app.error(
                   (ctx, cause, code) -> {
-                    ctx.render(new ModelAndView("error.hbs").put("statusCode", code));
+                    ctx.render(ModelAndView.map("error.hbs").put("statusCode", code));
                   });
             })
         .ready(
diff --git a/tests/src/test/java/io/jooby/test/TemplateEngineTest.java b/tests/src/test/java/io/jooby/test/TemplateEngineTest.java
index a2707b354..8297f3cf3 100644
--- a/tests/src/test/java/io/jooby/test/TemplateEngineTest.java
+++ b/tests/src/test/java/io/jooby/test/TemplateEngineTest.java
@@ -24,9 +24,9 @@ public class TemplateEngineTest {
               app.install(new HandlebarsModule());
               app.install(new FreemarkerModule());
 
-              app.get("/1", ctx -> new ModelAndView("index.hbs").put("name", "Handlebars"));
-              app.get("/2", ctx -> new ModelAndView("index.ftl").put("name", "Freemarker"));
-              app.get("/3", ctx -> new ModelAndView("index.html").put("name", "Thymeleaf"));
+              app.get("/1", ctx -> ModelAndView.map("index.hbs").put("name", "Handlebars"));
+              app.get("/2", ctx -> ModelAndView.map("index.ftl").put("name", "Freemarker"));
+              app.get("/3", ctx -> ModelAndView.map("index.html").put("name", "Thymeleaf"));
             })
         .ready(
             client -> {
@@ -65,7 +65,7 @@ public class TemplateEngineTest {
               app.install(
                   new ThymeleafModule(runner.resolvePath("src", "test", "resources", "views")));
 
-              app.get("/", ctx -> new ModelAndView("index.html").put("name", "Thymeleaf"));
+              app.get("/", ctx -> ModelAndView.map("index.html").put("name", "Thymeleaf"));
             })
         .ready(
             client -> {
@@ -94,7 +94,7 @@ public class TemplateEngineTest {
               app.install(
                   new HandlebarsModule(runner.resolvePath("src", "test", "resources", "views")));
 
-              app.get("/", ctx -> new ModelAndView("index.hbs").put("name", "Handlebars"));
+              app.get("/", ctx -> ModelAndView.map("index.hbs").put("name", "Handlebars"));
             })
         .ready(
             client -> {
@@ -114,7 +114,7 @@ public class TemplateEngineTest {
               app.install(
                   new FreemarkerModule(runner.resolvePath("src", "test", "resources", "views")));
 
-              app.get("/", ctx -> new ModelAndView("index.ftl").put("name", "Freemarker"));
+              app.get("/", ctx -> ModelAndView.map("index.ftl").put("name", "Freemarker"));
             })
         .ready(
             client -> {