tangcent / easy-yapi

Generate API document to yapi
https://easyyapi.com
GNU Affero General Public License v3.0
439 stars 144 forks source link

[Feature] We look forward to adding support for @JsonView. #1161

Closed YanzMing closed 1 month ago

YanzMing commented 2 months ago

Adding support for @JSONVIEW(期望添加对 @JSONVIEW 的支持)

Purpose of the requirement(需求目的)

Based on annotations, different documentation information can be generated.

(JsonView is a very convenient and powerful method that allows the use of models without duplicating resources. It determines the response field information by grouping with different views, eliminating the need to create redundant VOs)

As in the following example services A and B: 可以根据注解来生成不同的文档信息

(JsonView是一种非常方便且功能强大的方法,可以不复制资源使用的模型, 通过不同的视图分组来确定响应的字段信息,不需要创建冗余的 VO)

如以下示例服务A与服务B:

@GetMapping("/serviceA")
@JsonView(Entity.ViewA.class)
public Entity methodA() {...}

@GetMapping("/serviceB")
@JsonView(Entity.ViewB.class)
public Entity methodB() {...}

public Entity (){
    @JsonView(ViewA.class)
    private String A;   

    @JsonView(ViewB.class)
    private String B; 

    public interface ViewA{}
    public interface ViewB{}
}

The response contents of the above two services are as follows(以上两个服务的响应内容分别为):

RES_A:{
    A:""
}

RES_B:{
    B:""
}

In the current version, the documentation generated for the above two services will describe all fields, for example(当前版本对于以上两个服务所生成的文档会将所有字段进行描述,如):

RES_A:{
    A:"",
    B:""
}

RES_B:{
    A:"",
    B:""
}

What is expected for the documentation generated from the above two services is(对于以上两个服务生成的文档所期望的是):

RES_A:{
    A:""
}

RES_B:{
    B:""
}

When generating documentation, it is hoped to support @JSONVIEW. 在生成文档时希望支持@JSONVIEW,

Source of requirements(需求来源)

https://github.com/swagger-api/swagger-core/pull/2681

https://github.com/swagger-api/swagger-core/issues/479

https://github.com/swagger-api/swagger-core/pull/2662

tangcent commented 2 months ago

@YanzMing 你在用的是jackson里的JsonView吗? 方便的话可以尝试配一下这个规则,看看是否符合你的预期:

# Cache the JsonView information at the method level
api.method.parse.before=groovy:```
    def jsonViews = it.annValue("com.fasterxml.jackson.annotation.JsonView")
    //logger.info("method jsonViews:"+jsonViews)
    if (jsonViews) {
        session.set("json-views", jsonViews)
    }

api.method.parse.after=groovy:``` session.remove("json-views")


# Check if a field should be ignored based on the active JsonView
field.ignore=groovy:```
    if(it.contextType()!="field"){
        return false
    }
    def jsonViews = session.get("json-views")
    //logger.info("field jsonViews:"+jsonViews)
    if (jsonViews) {
        def fieldViews = it.annValue("com.fasterxml.jackson.annotation.JsonView")
        if (fieldViews) {
            // Return true if none of the field's views are in the active JsonView
            return !fieldViews.any{ fieldView-> jsonViews.any{ jsonView-> jsonView.isExtend(fieldView.name) } }
        } else {
            // If the field has no JsonView annotation, it should be ignored
            return true
        }
    }
    return false
YanzMing commented 2 months ago

这个规则生效了, 但是并没有完全生效, 如下:

@RestController
@RequestMapping("test")
public class Test {

    @JsonView(Person.A.class)
    @GetMapping("A")
    public Person testA(){
        return Person.builder().id(1L).name("测试01").age(18).wallet(BigDecimal.valueOf(1000)).build();
    }

    @JsonView(Person.A.B.class)
    @GetMapping("B")
    public Person testB(){
        return Person.builder().id(1L).name("测试01").age(18).wallet(BigDecimal.valueOf(1000)).build();
    }

    @JsonView(Person.A.C.class)
    @GetMapping("C")
    public Person testC(){
        return Person.builder().id(1L).name("测试01").age(18).wallet(BigDecimal.valueOf(1000)).build();
    }

    @Data
    @Builder
    static class Person{

        @JsonView(A.class)
        private Long id;

        @JsonView(A.B.class)
        private String name;

        @JsonView(A.B.class)
        private Integer age;

        @JsonView(A.C.class)
        private BigDecimal wallet;

        interface A{
            interface B extends A{}
            interface C extends A{}
        }
    }
}

A、B、C三个接口应该生成的相应结果如下:

// testA()
{
    "id": 1L
}

// testB()
{
    "id": 1L,
    "name": "测试01",
    "age": 18
}

// testC()
{
    "id": 1L,
    "wallet": 1000
}

但是,A、B、C三个接口生成的结果是相同的,如下

{
    "id": 1L,
    "wallet": 1000
}

这是不正确的!!!

测试得到,哪一个方法在最后面所生成的结果是对应最后一个方法的 (调整方法的编写顺序,如:testA() 在最后,三个方法结果都是 testA() 的结果响应)

tangcent commented 2 months ago

抱歉,漏了: json.cache.disable=true https://github.com/tangcent/easy-yapi/pull/1162/files#diff-c4538e37e8492c4ab2f8b12f61b888cabe1f699f67db269230feed70fa13efb1R136-R171

YanzMing commented 2 months ago

是的,它可以了

YanzMing commented 2 months ago

还存在一个问题, 他在入参中也生效了, 这是不对的, 入参不应该根据Jackson.JsonView来判断是否滤除, 而应该是Validated

tangcent commented 2 months ago

试试这样:

# Support for Jackson annotation JsonView
json.cache.disable=true
api.param.parse.before=groovy:session.set("is-param",true)
api.param.parse.after=groovy:session.remove("is-param")

# Cache the JsonView information at the method level
api.method.parse.before=groovy:```
    def jsonViews = it.annValue("com.fasterxml.jackson.annotation.JsonView")
    //logger.info("method jsonViews:"+jsonViews)
    if (jsonViews) {
        session.set("json-views", jsonViews)
    }

api.method.parse.after=groovy:``` session.remove("json-views")


# Check if a field should be ignored based on the active JsonView
field.ignore=groovy:```
    if(session.get("is-param")){
        return false
    }
    if(it.contextType()!="field"){
        return false
    }
    def jsonViews = session.get("json-views")
    //logger.info("field jsonViews:"+jsonViews)
    if (jsonViews) {
        def fieldViews = it.annValue("com.fasterxml.jackson.annotation.JsonView")
        if (fieldViews) {
            // Return true if none of the field's views are in the active JsonView
            return !fieldViews.any{ fieldView-> jsonViews.any{ jsonView-> jsonView.isExtend(fieldView.name) } }
        } else {
            // If the field has no JsonView annotation, it should be ignored
            return true
        }
    }
    return false
YanzMing commented 2 months ago

好像导致validation失效了

YanzMing commented 2 months ago
@Validated
@RestController
@RequestMapping("test")
public class Test {

    @JsonView(Person.A.class)
    @PostMapping("A")
    public Person testA(@RequestBody @Validated(Person.A.class) Person person) {
        return person;
    }

    @JsonView(Person.A.B.class)
    @PostMapping("B")
    public Person testB(@RequestBody Person person) {
        return person;
    }

    @JsonView(Person.A.C.class)
    @PostMapping("C")
    public Person testC(@RequestBody Person person) {
        return person;
    }

    @JsonView(Person.D.class)
    @PostMapping("D")
    public Person testD(@RequestBody Person person) {
        return person;
    }

    @PostMapping("E")
    @JsonView(Person.E.class)
    public Person testE(@RequestBody Person person) {
        return person;
    }
}
@Data
@GroupSequenceProvider(Person.PersonGroupSequenceProvider.class)
public class Person {

    @JsonView(A.class)
    @Null(groups = A.class)
    private Long id;

    @JsonView({A.class, D.class, E.class})
    @NotNull
    private String name;

    @JsonView({A.B.class, D.class})
    @Null
    private Integer age;

    @JsonView({A.C.class, E.class})
    @NotNull(groups = {WhenAge20And30Group.class, WhenAge30And40Group.class})
    private BigDecimal wallet;

    public interface A {
        interface B extends A {}
        interface C extends A {}
    }
    public interface D {}
    public interface E {}
    interface WhenAge20And30Group {}
    interface WhenAge30And40Group {}

    // 组序列
    @GroupSequence({Default.class, WhenAge20And30Group.class, WhenAge30And40Group.class})
    interface Group{}

    static class PersonGroupSequenceProvider implements DefaultGroupSequenceProvider<Person> {
        @Override
        public List<Class<?>> getValidationGroups(Person bean) {
            List<Class<?>> defaultGroupSequence = new ArrayList<>();
            defaultGroupSequence.add(Person.class); 
            if (bean != null) {
                Integer age = bean.getAge();
                if (age >= 20 && age < 30) {
                    defaultGroupSequence.add(Person.WhenAge20And30Group.class);
                } else if (age >= 30 && age < 40) {
                    defaultGroupSequence.add(Person.WhenAge30And40Group.class);
                }
            }
            return defaultGroupSequence;
        }
    }
}
YanzMing commented 2 months ago

在使用@JsonView的情况下, 如何修改响应类的类名?


    @JsonView(Person.AView.class)
    @PostMapping("A")
    public Person testA(@RequestBody @Validated(Person.A.class) Person person) {
        return person;
    }

如: 一个被@JsonView(Person.AView.class)注解的方法, 在文档中生成响应为Person#AView而不是Person

tangcent commented 2 months ago
GroupSequence

这个有点复杂了,难搞 :)

tangcent commented 2 months ago

在使用@JSONVIEW的情况下, 如何修改响应类的类名?

    @JsonView(Person.AView.class)
    @PostMapping("A")
    public Person testA(@RequestBody @Validated(Person.A.class) Person person) {
        return person;
    }

如: 一个被@JsonView(Person.AView.class)注解的方法, 在文档中生成响应为Person#AView而不是Person

你的意思是这里注解的Person.AView.class不是一个group class,而是一个POJO class?